@buape/carbon 0.14.0 → 0.16.0
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.
- package/LICENSE +1 -1
- package/dist/package.json +11 -9
- package/dist/src/abstracts/BaseChannel.d.ts +24 -2
- package/dist/src/abstracts/BaseChannel.d.ts.map +1 -1
- package/dist/src/abstracts/BaseChannel.js +45 -4
- package/dist/src/abstracts/BaseChannel.js.map +1 -1
- package/dist/src/abstracts/BaseCommand.d.ts +15 -3
- package/dist/src/abstracts/BaseCommand.d.ts.map +1 -1
- package/dist/src/abstracts/BaseCommand.js +32 -3
- package/dist/src/abstracts/BaseCommand.js.map +1 -1
- package/dist/src/abstracts/BaseInteraction.d.ts +1 -1
- package/dist/src/abstracts/BaseInteraction.d.ts.map +1 -1
- package/dist/src/abstracts/BaseInteraction.js +3 -3
- package/dist/src/abstracts/BaseInteraction.js.map +1 -1
- package/dist/src/abstracts/BaseListener.d.ts +7 -4
- package/dist/src/abstracts/BaseListener.d.ts.map +1 -1
- package/dist/src/abstracts/BaseListener.js.map +1 -1
- package/dist/src/abstracts/Plugin.d.ts +6 -0
- package/dist/src/abstracts/Plugin.d.ts.map +1 -1
- package/dist/src/abstracts/Plugin.js.map +1 -1
- package/dist/src/adapters/bun/index.d.ts +1 -1
- package/dist/src/adapters/bun/index.d.ts.map +1 -1
- package/dist/src/adapters/bun/index.js +6 -0
- package/dist/src/adapters/bun/index.js.map +1 -1
- package/dist/src/adapters/fetch/index.d.ts.map +1 -1
- package/dist/src/adapters/fetch/index.js +6 -0
- package/dist/src/adapters/fetch/index.js.map +1 -1
- package/dist/src/classes/Client.d.ts +97 -6
- package/dist/src/classes/Client.d.ts.map +1 -1
- package/dist/src/classes/Client.js +187 -12
- package/dist/src/classes/Client.js.map +1 -1
- package/dist/src/classes/EntryPointCommand.d.ts +11 -0
- package/dist/src/classes/EntryPointCommand.d.ts.map +1 -0
- package/dist/src/classes/EntryPointCommand.js +13 -0
- package/dist/src/classes/EntryPointCommand.js.map +1 -0
- package/dist/src/classes/Listener.d.ts +83 -81
- package/dist/src/classes/Listener.d.ts.map +1 -1
- package/dist/src/classes/Listener.js +15 -4
- package/dist/src/classes/Listener.js.map +1 -1
- package/dist/src/classes/RequestClient.d.ts +92 -17
- package/dist/src/classes/RequestClient.d.ts.map +1 -1
- package/dist/src/classes/RequestClient.js +207 -136
- package/dist/src/classes/RequestClient.js.map +1 -1
- package/dist/src/errors/RatelimitError.d.ts +4 -1
- package/dist/src/errors/RatelimitError.d.ts.map +1 -1
- package/dist/src/errors/RatelimitError.js +7 -1
- package/dist/src/errors/RatelimitError.js.map +1 -1
- package/dist/src/functions/channelFactory.d.ts +4 -12
- package/dist/src/functions/channelFactory.d.ts.map +1 -1
- package/dist/src/functions/channelFactory.js +39 -13
- package/dist/src/functions/channelFactory.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/internals/BoundedExecutor.d.ts +12 -0
- package/dist/src/internals/BoundedExecutor.d.ts.map +1 -0
- package/dist/src/internals/BoundedExecutor.js +60 -0
- package/dist/src/internals/BoundedExecutor.js.map +1 -0
- package/dist/src/internals/CommandHandler.d.ts.map +1 -1
- package/dist/src/internals/CommandHandler.js +53 -6
- package/dist/src/internals/CommandHandler.js.map +1 -1
- package/dist/src/internals/ComponentHandler.d.ts +2 -1
- package/dist/src/internals/ComponentHandler.d.ts.map +1 -1
- package/dist/src/internals/ComponentHandler.js +11 -4
- package/dist/src/internals/ComponentHandler.js.map +1 -1
- package/dist/src/internals/EventHandler.d.ts +18 -3
- package/dist/src/internals/EventHandler.d.ts.map +1 -1
- package/dist/src/internals/EventQueue.d.ts +43 -12
- package/dist/src/internals/EventQueue.d.ts.map +1 -1
- package/dist/src/internals/EventQueue.js +294 -62
- package/dist/src/internals/EventQueue.js.map +1 -1
- package/dist/src/internals/FieldsHandler.d.ts.map +1 -1
- package/dist/src/internals/FieldsHandler.js +10 -1
- package/dist/src/internals/FieldsHandler.js.map +1 -1
- package/dist/src/internals/RequestBody.d.ts +6 -0
- package/dist/src/internals/RequestBody.d.ts.map +1 -0
- package/dist/src/internals/RequestBody.js +74 -0
- package/dist/src/internals/RequestBody.js.map +1 -0
- package/dist/src/internals/RequestScheduler.d.ts +131 -0
- package/dist/src/internals/RequestScheduler.d.ts.map +1 -0
- package/dist/src/internals/RequestScheduler.js +245 -0
- package/dist/src/internals/RequestScheduler.js.map +1 -0
- package/dist/src/internals/TemporaryListenerManager.d.ts +3 -3
- package/dist/src/internals/TemporaryListenerManager.d.ts.map +1 -1
- package/dist/src/internals/TemporaryListenerManager.js +1 -1
- package/dist/src/internals/TemporaryListenerManager.js.map +1 -1
- package/dist/src/plugins/client-manager/ClientManager.d.ts +30 -2
- package/dist/src/plugins/client-manager/ClientManager.d.ts.map +1 -1
- package/dist/src/plugins/client-manager/ClientManager.js +86 -16
- package/dist/src/plugins/client-manager/ClientManager.js.map +1 -1
- package/dist/src/plugins/cloudflare-gateway/CloudflareGatewayDurableObject.d.ts +43 -0
- package/dist/src/plugins/cloudflare-gateway/CloudflareGatewayDurableObject.d.ts.map +1 -0
- package/dist/src/plugins/cloudflare-gateway/CloudflareGatewayDurableObject.js +210 -0
- package/dist/src/plugins/cloudflare-gateway/CloudflareGatewayDurableObject.js.map +1 -0
- package/dist/src/plugins/cloudflare-gateway/CloudflareGatewayPlugin.d.ts +16 -0
- package/dist/src/plugins/cloudflare-gateway/CloudflareGatewayPlugin.d.ts.map +1 -0
- package/dist/src/plugins/cloudflare-gateway/CloudflareGatewayPlugin.js +129 -0
- package/dist/src/plugins/cloudflare-gateway/CloudflareGatewayPlugin.js.map +1 -0
- package/dist/src/plugins/cloudflare-gateway/index.d.ts +4 -0
- package/dist/src/plugins/cloudflare-gateway/index.d.ts.map +1 -0
- package/dist/src/plugins/cloudflare-gateway/index.js +4 -0
- package/dist/src/plugins/cloudflare-gateway/index.js.map +1 -0
- package/dist/src/plugins/cloudflare-gateway/types.d.ts +63 -0
- package/dist/src/plugins/cloudflare-gateway/types.d.ts.map +1 -0
- package/dist/src/plugins/cloudflare-gateway/types.js +2 -0
- package/dist/src/plugins/cloudflare-gateway/types.js.map +1 -0
- package/dist/src/plugins/gateway/GatewayPlugin.d.ts +108 -19
- package/dist/src/plugins/gateway/GatewayPlugin.d.ts.map +1 -1
- package/dist/src/plugins/gateway/GatewayPlugin.js +564 -253
- package/dist/src/plugins/gateway/GatewayPlugin.js.map +1 -1
- package/dist/src/plugins/gateway/types.d.ts +33 -1
- package/dist/src/plugins/gateway/types.d.ts.map +1 -1
- package/dist/src/plugins/gateway/types.js +21 -0
- package/dist/src/plugins/gateway/types.js.map +1 -1
- package/dist/src/plugins/gateway/utils/heartbeat.d.ts.map +1 -1
- package/dist/src/plugins/gateway/utils/heartbeat.js +10 -4
- package/dist/src/plugins/gateway/utils/heartbeat.js.map +1 -1
- package/dist/src/plugins/gateway/utils/payload.d.ts.map +1 -1
- package/dist/src/plugins/gateway/utils/payload.js +0 -4
- package/dist/src/plugins/gateway/utils/payload.js.map +1 -1
- package/dist/src/plugins/gateway-forwarder/GatewayForwarderPlugin.d.ts +56 -3
- package/dist/src/plugins/gateway-forwarder/GatewayForwarderPlugin.d.ts.map +1 -1
- package/dist/src/plugins/gateway-forwarder/GatewayForwarderPlugin.js +224 -21
- package/dist/src/plugins/gateway-forwarder/GatewayForwarderPlugin.js.map +1 -1
- package/dist/src/plugins/paginator/index.d.ts +1 -1
- package/dist/src/plugins/paginator/index.d.ts.map +1 -1
- package/dist/src/plugins/paginator/index.js +1 -1
- package/dist/src/plugins/paginator/index.js.map +1 -1
- package/dist/src/plugins/voice/VoicePlugin.d.ts.map +1 -1
- package/dist/src/plugins/voice/VoicePlugin.js +5 -1
- package/dist/src/plugins/voice/VoicePlugin.js.map +1 -1
- package/dist/src/plugins/voice/VoiceServerUpdateListener.d.ts +10 -0
- package/dist/src/plugins/voice/VoiceServerUpdateListener.d.ts.map +1 -0
- package/dist/src/plugins/voice/VoiceServerUpdateListener.js +16 -0
- package/dist/src/plugins/voice/VoiceServerUpdateListener.js.map +1 -0
- package/dist/src/plugins/voice/VoiceStateUpdateListener.d.ts +10 -0
- package/dist/src/plugins/voice/VoiceStateUpdateListener.d.ts.map +1 -0
- package/dist/src/plugins/voice/VoiceStateUpdateListener.js +21 -0
- package/dist/src/plugins/voice/VoiceStateUpdateListener.js.map +1 -0
- package/dist/src/structures/GroupDmChannel.d.ts +5 -1
- package/dist/src/structures/GroupDmChannel.d.ts.map +1 -1
- package/dist/src/structures/GroupDmChannel.js +10 -1
- package/dist/src/structures/GroupDmChannel.js.map +1 -1
- package/dist/src/structures/Guild.d.ts +13 -4
- package/dist/src/structures/Guild.d.ts.map +1 -1
- package/dist/src/structures/Guild.js +9 -1
- package/dist/src/structures/Guild.js.map +1 -1
- package/dist/src/structures/GuildMediaChannel.d.ts +1 -1
- package/dist/src/structures/GuildMediaChannel.d.ts.map +1 -1
- package/dist/src/structures/GuildMediaChannel.js.map +1 -1
- package/dist/src/structures/Message.d.ts +1 -1
- package/dist/src/structures/Poll.d.ts +1 -1
- package/dist/src/structures/Poll.d.ts.map +1 -1
- package/dist/src/structures/Webhook.d.ts +4 -2
- package/dist/src/structures/Webhook.d.ts.map +1 -1
- package/dist/src/structures/Webhook.js +42 -29
- package/dist/src/structures/Webhook.js.map +1 -1
- package/dist/src/types/channels.d.ts +33 -0
- package/dist/src/types/channels.d.ts.map +1 -0
- package/dist/src/types/channels.js +2 -0
- package/dist/src/types/channels.js.map +1 -0
- package/dist/src/types/commandMiddleware.d.ts +64 -0
- package/dist/src/types/commandMiddleware.d.ts.map +1 -0
- package/dist/src/types/commandMiddleware.js +2 -0
- package/dist/src/types/commandMiddleware.js.map +1 -0
- package/dist/src/types/index.d.ts +10 -5
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js +2 -0
- package/dist/src/types/index.js.map +1 -1
- package/dist/src/types/listeners.d.ts +13 -4
- package/dist/src/types/listeners.d.ts.map +1 -1
- package/dist/src/types/listeners.js.map +1 -1
- package/dist/src/utils/payload.d.ts.map +1 -1
- package/dist/src/utils/payload.js +16 -0
- package/dist/src/utils/payload.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -9
- package/dist/src/adapters/node/index.d.ts +0 -13
- package/dist/src/adapters/node/index.d.ts.map +0 -1
- package/dist/src/adapters/node/index.js +0 -17
- package/dist/src/adapters/node/index.js.map +0 -1
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
3
|
import { Plugin } from "../../abstracts/Plugin.js";
|
|
4
|
-
import { ListenerEvent } from "../../types/index.js";
|
|
5
4
|
import { BabyCache } from "./BabyCache.js";
|
|
6
5
|
import { InteractionEventListener } from "./InteractionEventListener.js";
|
|
7
|
-
import {
|
|
6
|
+
import { fatalGatewayCloseCodes, GatewayIntents, GatewayOpcodes, listenerEvents, nonResumableGatewayCloseCodes, reconnectDefaults } from "./types.js";
|
|
8
7
|
import { startHeartbeat, stopHeartbeat } from "./utils/heartbeat.js";
|
|
9
8
|
import { ConnectionMonitor } from "./utils/monitor.js";
|
|
10
9
|
import { createIdentifyPayload, createRequestGuildMembersPayload, createResumePayload, createUpdatePresencePayload, createUpdateVoiceStatePayload, validatePayload } from "./utils/payload.js";
|
|
11
10
|
import { GatewayRateLimit } from "./utils/rateLimit.js";
|
|
11
|
+
const textDecoder = new TextDecoder();
|
|
12
|
+
const socketOpenState = 1;
|
|
13
|
+
const nodeRequire = typeof process !== "undefined" && process.versions?.node
|
|
14
|
+
? createRequire(import.meta.url)
|
|
15
|
+
: null;
|
|
12
16
|
export class GatewayPlugin extends Plugin {
|
|
13
17
|
id = "gateway";
|
|
14
18
|
client;
|
|
@@ -18,6 +22,7 @@ export class GatewayPlugin extends Plugin {
|
|
|
18
22
|
monitor;
|
|
19
23
|
rateLimit;
|
|
20
24
|
heartbeatInterval;
|
|
25
|
+
firstHeartbeatTimeout;
|
|
21
26
|
sequence = null;
|
|
22
27
|
lastHeartbeatAck = true;
|
|
23
28
|
emitter;
|
|
@@ -30,15 +35,20 @@ export class GatewayPlugin extends Plugin {
|
|
|
30
35
|
babyCache;
|
|
31
36
|
reconnectTimeout;
|
|
32
37
|
isConnecting = false;
|
|
38
|
+
socketGeneration = 0;
|
|
39
|
+
shouldReconnect = false;
|
|
40
|
+
nextConnectionShouldResume = false;
|
|
41
|
+
silentSocketClosures = new WeakSet();
|
|
42
|
+
consecutiveResumeFailures = 0;
|
|
33
43
|
constructor(options, gatewayInfo) {
|
|
34
44
|
super();
|
|
35
45
|
this.options = {
|
|
36
46
|
reconnect: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
maxDelay: 30000
|
|
47
|
+
...reconnectDefaults,
|
|
48
|
+
...options.reconnect
|
|
40
49
|
},
|
|
41
|
-
...options
|
|
50
|
+
...options,
|
|
51
|
+
intents: options.intents ?? 0
|
|
42
52
|
};
|
|
43
53
|
this.state = {
|
|
44
54
|
sequence: null,
|
|
@@ -58,305 +68,265 @@ export class GatewayPlugin extends Plugin {
|
|
|
58
68
|
? this.pings.reduce((a, b) => a + b, 0) / this.pings.length
|
|
59
69
|
: null;
|
|
60
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Bootstraps gateway metadata and opens the initial websocket connection.
|
|
73
|
+
*/
|
|
61
74
|
async registerClient(client) {
|
|
62
75
|
this.client = client;
|
|
63
76
|
if (!this.gatewayInfo) {
|
|
77
|
+
let response;
|
|
64
78
|
try {
|
|
65
|
-
|
|
79
|
+
response = await fetch("https://discord.com/api/v10/gateway/bot", {
|
|
66
80
|
headers: {
|
|
67
81
|
Authorization: `Bot ${client.options.token}`
|
|
68
82
|
}
|
|
69
83
|
});
|
|
70
|
-
this.gatewayInfo = (await response.json());
|
|
71
84
|
}
|
|
72
85
|
catch (error) {
|
|
73
|
-
throw new Error(`Failed to get gateway information from Discord: ${error instanceof Error ? error.message : String(error)}
|
|
86
|
+
throw new Error(`Failed to get gateway information from Discord: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
74
87
|
}
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
throw new Error(`Failed to get gateway information from Discord: ${response.status} ${response.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
this.gatewayInfo = (await response.json());
|
|
75
92
|
}
|
|
76
|
-
// Set shard information on the client
|
|
77
93
|
if (this.options.shard) {
|
|
78
94
|
client.shardId = this.options.shard[0];
|
|
79
95
|
client.totalShards = this.options.shard[1];
|
|
80
96
|
}
|
|
81
97
|
if (this.options.autoInteractions) {
|
|
82
|
-
this.client
|
|
98
|
+
this.client.registerListener(new InteractionEventListener());
|
|
83
99
|
}
|
|
84
|
-
this.
|
|
100
|
+
this.shouldReconnect = true;
|
|
101
|
+
this.connect(false);
|
|
85
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Opens a new websocket connection and prepares either IDENTIFY or RESUME on HELLO.
|
|
105
|
+
*/
|
|
86
106
|
connect(resume = false) {
|
|
87
|
-
if (this.isConnecting)
|
|
107
|
+
if (this.isConnecting) {
|
|
88
108
|
return;
|
|
89
|
-
if (this.reconnectTimeout) {
|
|
90
|
-
clearTimeout(this.reconnectTimeout);
|
|
91
|
-
this.reconnectTimeout = undefined;
|
|
92
109
|
}
|
|
93
|
-
this.
|
|
110
|
+
this.shouldReconnect = true;
|
|
111
|
+
this.clearReconnectTimeout();
|
|
112
|
+
stopHeartbeat(this);
|
|
113
|
+
this.lastHeartbeatAck = true;
|
|
114
|
+
const oldSocket = this.ws;
|
|
115
|
+
if (oldSocket) {
|
|
116
|
+
this.silentSocketClosures.add(oldSocket);
|
|
117
|
+
this.closeSocketImmediately(oldSocket);
|
|
118
|
+
}
|
|
94
119
|
const baseUrl = resume && this.state.resumeGatewayUrl
|
|
95
120
|
? this.state.resumeGatewayUrl
|
|
96
121
|
: (this.gatewayInfo?.url ??
|
|
97
122
|
this.options.url ??
|
|
98
123
|
"wss://gateway.discord.gg/");
|
|
99
124
|
const url = this.ensureGatewayParams(baseUrl);
|
|
125
|
+
this.nextConnectionShouldResume = resume;
|
|
126
|
+
this.socketGeneration++;
|
|
100
127
|
this.ws = this.createWebSocket(url);
|
|
101
128
|
this.isConnecting = true;
|
|
129
|
+
this.isConnected = false;
|
|
102
130
|
this.setupWebSocket();
|
|
103
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Stops heartbeats, clears reconnect state, and closes the active socket intentionally.
|
|
134
|
+
*/
|
|
104
135
|
disconnect() {
|
|
136
|
+
this.shouldReconnect = false;
|
|
137
|
+
this.clearReconnectTimeout();
|
|
105
138
|
stopHeartbeat(this);
|
|
139
|
+
this.lastHeartbeatAck = true;
|
|
106
140
|
this.monitor.resetUptime();
|
|
107
|
-
this.
|
|
108
|
-
this.ws
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.reconnectTimeout = undefined;
|
|
141
|
+
this.rateLimit.reset();
|
|
142
|
+
if (this.ws) {
|
|
143
|
+
this.silentSocketClosures.add(this.ws);
|
|
144
|
+
this.ws.close(1000, "Client disconnect");
|
|
112
145
|
}
|
|
146
|
+
this.ws = null;
|
|
113
147
|
this.isConnecting = false;
|
|
114
148
|
this.isConnected = false;
|
|
149
|
+
this.reconnectAttempts = 0;
|
|
150
|
+
this.consecutiveResumeFailures = 0;
|
|
115
151
|
this.pings = [];
|
|
116
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Creates the websocket instance for a gateway URL. Override in tests if needed.
|
|
155
|
+
*/
|
|
117
156
|
createWebSocket(url) {
|
|
118
157
|
if (!url) {
|
|
119
158
|
throw new Error("Gateway URL is required");
|
|
120
159
|
}
|
|
121
|
-
|
|
160
|
+
const socket = this.options.webSocketFactory
|
|
161
|
+
? this.options.webSocketFactory(url)
|
|
162
|
+
: (() => {
|
|
163
|
+
if (nodeRequire) {
|
|
164
|
+
try {
|
|
165
|
+
const wsModule = nodeRequire("ws");
|
|
166
|
+
const nodeWebSocket = typeof wsModule.WebSocket === "function"
|
|
167
|
+
? wsModule.WebSocket
|
|
168
|
+
: typeof wsModule.default === "function"
|
|
169
|
+
? wsModule.default
|
|
170
|
+
: null;
|
|
171
|
+
if (nodeWebSocket) {
|
|
172
|
+
return new nodeWebSocket(url);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// fall through to global WebSocket
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (typeof globalThis.WebSocket === "function") {
|
|
180
|
+
return new globalThis.WebSocket(url);
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
})();
|
|
184
|
+
if (!socket) {
|
|
185
|
+
throw new Error("No WebSocket implementation available. Provide GatewayPluginOptions.webSocketFactory or install 'ws'.");
|
|
186
|
+
}
|
|
187
|
+
if ("binaryType" in socket) {
|
|
188
|
+
try {
|
|
189
|
+
;
|
|
190
|
+
socket.binaryType = "arraybuffer";
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Ignore runtimes that expose a readonly binaryType.
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return socket;
|
|
122
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Attaches websocket lifecycle handlers for open, message, close, and error.
|
|
200
|
+
*/
|
|
123
201
|
setupWebSocket() {
|
|
124
|
-
if (!this.ws)
|
|
202
|
+
if (!this.ws) {
|
|
125
203
|
return;
|
|
126
|
-
|
|
127
|
-
this.ws
|
|
204
|
+
}
|
|
205
|
+
const socket = this.ws;
|
|
206
|
+
const generation = this.socketGeneration;
|
|
207
|
+
this.onSocketEvent(socket, "open", () => {
|
|
208
|
+
if (!this.isCurrentSocket(socket, generation)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
128
211
|
this.isConnecting = false;
|
|
129
|
-
this.
|
|
130
|
-
this.emitter.emit("debug", "WebSocket connection opened");
|
|
212
|
+
this.emitter.emit("debug", "Gateway websocket opened");
|
|
131
213
|
});
|
|
132
|
-
this.
|
|
214
|
+
this.onSocketEvent(socket, "message", (incoming) => {
|
|
215
|
+
if (!this.isCurrentSocket(socket, generation)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
133
218
|
this.monitor.recordMessageReceived();
|
|
134
|
-
const
|
|
219
|
+
const payloadText = this.getMessageText(incoming);
|
|
220
|
+
const payload = payloadText ? validatePayload(payloadText) : null;
|
|
135
221
|
if (!payload) {
|
|
136
222
|
this.monitor.recordError();
|
|
137
|
-
this.emitter.emit("error", new Error("Invalid gateway payload
|
|
223
|
+
this.emitter.emit("error", new Error("Invalid gateway payload"));
|
|
138
224
|
return;
|
|
139
225
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.sequence = s;
|
|
143
|
-
this.state.sequence = s;
|
|
226
|
+
if (payload.s !== null && payload.s !== undefined) {
|
|
227
|
+
this.sequence = payload.s;
|
|
228
|
+
this.state.sequence = payload.s;
|
|
144
229
|
}
|
|
145
|
-
switch (op) {
|
|
146
|
-
case GatewayOpcodes.Hello:
|
|
147
|
-
|
|
148
|
-
const interval = helloData.heartbeat_interval;
|
|
149
|
-
startHeartbeat(this, {
|
|
150
|
-
interval,
|
|
151
|
-
reconnectCallback: () => {
|
|
152
|
-
if (closed) {
|
|
153
|
-
throw new Error("Attempted to reconnect zombie connection after disconnecting first (this shouldn't be possible)");
|
|
154
|
-
}
|
|
155
|
-
closed = true;
|
|
156
|
-
this.handleZombieConnection();
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
if (this.canResume()) {
|
|
160
|
-
this.resume();
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
this.identify();
|
|
164
|
-
}
|
|
230
|
+
switch (payload.op) {
|
|
231
|
+
case GatewayOpcodes.Hello:
|
|
232
|
+
this.handleHello(payload.d, generation);
|
|
165
233
|
break;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.lastHeartbeatAck = true;
|
|
169
|
-
this.monitor.recordHeartbeatAck();
|
|
170
|
-
// Record the latency for ping averaging
|
|
171
|
-
const latency = this.monitor.getMetrics().latency;
|
|
172
|
-
if (latency > 0) {
|
|
173
|
-
this.pings.push(latency);
|
|
174
|
-
// Keep only the last 10 pings to prevent unbounded growth
|
|
175
|
-
if (this.pings.length > 10) {
|
|
176
|
-
this.pings.shift();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
234
|
+
case GatewayOpcodes.HeartbeatAck:
|
|
235
|
+
this.handleHeartbeatAck();
|
|
179
236
|
break;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.lastHeartbeatAck = false;
|
|
183
|
-
this.send({
|
|
184
|
-
op: GatewayOpcodes.Heartbeat,
|
|
185
|
-
d: this.sequence
|
|
186
|
-
});
|
|
237
|
+
case GatewayOpcodes.Heartbeat:
|
|
238
|
+
this.sendHeartbeatNow();
|
|
187
239
|
break;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const payload1 = payload;
|
|
191
|
-
const t1 = payload1.t;
|
|
192
|
-
try {
|
|
193
|
-
if (!Object.values(ListenerEvent).includes(t1)) {
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
if (t1 === "READY") {
|
|
197
|
-
const readyData = d;
|
|
198
|
-
this.state.sessionId = readyData.session_id;
|
|
199
|
-
this.state.resumeGatewayUrl = readyData.resume_gateway_url;
|
|
200
|
-
}
|
|
201
|
-
if (t && this.client) {
|
|
202
|
-
if (!this.options.eventFilter || this.options.eventFilter?.(t1)) {
|
|
203
|
-
if (t1 === "READY" || t1 === "RESUMED") {
|
|
204
|
-
this.isConnected = true;
|
|
205
|
-
}
|
|
206
|
-
if (t1 === "READY") {
|
|
207
|
-
const readyData = d;
|
|
208
|
-
readyData.guilds.forEach((guild) => {
|
|
209
|
-
this.babyCache.setGuild(guild.id, {
|
|
210
|
-
available: false,
|
|
211
|
-
lastEvent: Date.now()
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
if (t1 === "GUILD_CREATE") {
|
|
216
|
-
const guildCreateData = d;
|
|
217
|
-
const existingGuild = this.babyCache.getGuild(guildCreateData.id);
|
|
218
|
-
if (existingGuild && !existingGuild.available) {
|
|
219
|
-
this.babyCache.setGuild(guildCreateData.id, {
|
|
220
|
-
available: true,
|
|
221
|
-
lastEvent: Date.now()
|
|
222
|
-
});
|
|
223
|
-
this.client.eventHandler.handleEvent({
|
|
224
|
-
...guildCreateData,
|
|
225
|
-
clientId: this.client.options.clientId
|
|
226
|
-
}, "GUILD_AVAILABLE");
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (t1 === "GUILD_DELETE") {
|
|
231
|
-
const guildDeleteData = d;
|
|
232
|
-
const existingGuild = this.babyCache.getGuild(guildDeleteData.id);
|
|
233
|
-
if (existingGuild?.available && guildDeleteData.unavailable) {
|
|
234
|
-
this.babyCache.setGuild(guildDeleteData.id, {
|
|
235
|
-
available: false,
|
|
236
|
-
lastEvent: Date.now()
|
|
237
|
-
});
|
|
238
|
-
this.client.eventHandler.handleEvent({
|
|
239
|
-
...guildDeleteData,
|
|
240
|
-
clientId: this.client.options.clientId
|
|
241
|
-
}, "GUILD_UNAVAILABLE");
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
this.client.eventHandler.handleEvent({ ...payload1.d, clientId: this.client.options.clientId }, t1);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
catch (err) {
|
|
250
|
-
console.error(err);
|
|
251
|
-
}
|
|
240
|
+
case GatewayOpcodes.Dispatch:
|
|
241
|
+
this.handleDispatch(payload);
|
|
252
242
|
break;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const canResume = Boolean(d);
|
|
256
|
-
setTimeout(() => {
|
|
257
|
-
closed = true;
|
|
258
|
-
if (canResume && this.canResume()) {
|
|
259
|
-
this.connect(true);
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
this.state.sessionId = null;
|
|
263
|
-
this.state.resumeGatewayUrl = null;
|
|
264
|
-
this.state.sequence = null;
|
|
265
|
-
this.sequence = null;
|
|
266
|
-
this.pings = [];
|
|
267
|
-
this.connect(false);
|
|
268
|
-
}
|
|
269
|
-
}, 5000);
|
|
243
|
+
case GatewayOpcodes.InvalidSession:
|
|
244
|
+
this.handleInvalidSession(payload.d);
|
|
270
245
|
break;
|
|
271
|
-
}
|
|
272
246
|
case GatewayOpcodes.Reconnect:
|
|
273
|
-
|
|
274
|
-
throw new Error("Attempted to reconnect gateway after disconnecting first (this shouldn't be possible)");
|
|
275
|
-
}
|
|
276
|
-
closed = true;
|
|
277
|
-
this.state.sequence = this.sequence;
|
|
278
|
-
this.ws?.close();
|
|
279
|
-
this.handleReconnect();
|
|
247
|
+
this.handleReconnectOpcode();
|
|
280
248
|
break;
|
|
281
249
|
}
|
|
282
250
|
});
|
|
283
|
-
this.
|
|
251
|
+
this.onSocketEvent(socket, "close", (incoming) => {
|
|
252
|
+
if (!this.isCurrentSocket(socket, generation)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
284
255
|
this.isConnecting = false;
|
|
285
|
-
this.
|
|
286
|
-
this
|
|
287
|
-
|
|
256
|
+
this.isConnected = false;
|
|
257
|
+
stopHeartbeat(this);
|
|
258
|
+
this.lastHeartbeatAck = true;
|
|
259
|
+
const wasSilentClose = this.silentSocketClosures.has(socket);
|
|
260
|
+
if (wasSilentClose) {
|
|
261
|
+
this.silentSocketClosures.delete(socket);
|
|
288
262
|
return;
|
|
289
|
-
|
|
263
|
+
}
|
|
264
|
+
const code = this.getCloseCode(incoming);
|
|
265
|
+
this.monitor.recordReconnect();
|
|
266
|
+
this.emitter.emit("debug", `Gateway websocket closed: ${code}`);
|
|
290
267
|
this.handleClose(code);
|
|
291
268
|
});
|
|
292
|
-
this.
|
|
269
|
+
this.onSocketEvent(socket, "error", (incoming) => {
|
|
270
|
+
if (!this.isCurrentSocket(socket, generation)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
293
273
|
this.isConnecting = false;
|
|
294
274
|
this.monitor.recordError();
|
|
295
|
-
this.emitter.emit("error",
|
|
275
|
+
this.emitter.emit("error", this.getSocketError(incoming));
|
|
296
276
|
});
|
|
297
277
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Handles close codes and decides whether reconnection should resume or re-identify.
|
|
280
|
+
*/
|
|
281
|
+
handleClose(code) {
|
|
282
|
+
if (!this.shouldReconnect) {
|
|
303
283
|
return;
|
|
304
284
|
}
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
case GatewayCloseCodes.InvalidIntents:
|
|
310
|
-
case GatewayCloseCodes.DisallowedIntents:
|
|
311
|
-
case GatewayCloseCodes.ShardingRequired: {
|
|
312
|
-
this.emitter.emit("error", new Error(`Fatal Gateway error: ${options.code}`));
|
|
313
|
-
this.reconnectAttempts = maxAttempts;
|
|
314
|
-
this.monitor.destroy();
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
case GatewayCloseCodes.InvalidSeq:
|
|
318
|
-
case GatewayCloseCodes.SessionTimedOut: {
|
|
319
|
-
this.state.sessionId = null;
|
|
320
|
-
this.state.resumeGatewayUrl = null;
|
|
321
|
-
this.state.sequence = null;
|
|
322
|
-
this.sequence = null;
|
|
323
|
-
this.pings = [];
|
|
324
|
-
options.forceNoResume = true;
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (this.reconnectTimeout || this.isConnecting) {
|
|
285
|
+
if (fatalGatewayCloseCodes.has(code)) {
|
|
286
|
+
this.shouldReconnect = false;
|
|
287
|
+
this.emitter.emit("error", new Error(`Fatal gateway close code: ${code}`));
|
|
288
|
+
this.disconnect();
|
|
330
289
|
return;
|
|
331
290
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.reconnectAttempts++;
|
|
335
|
-
if (options.isZombieConnection) {
|
|
336
|
-
this.monitor.recordZombieConnection();
|
|
291
|
+
if (nonResumableGatewayCloseCodes.has(code)) {
|
|
292
|
+
this.resetSessionState();
|
|
337
293
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}, backoffTime);
|
|
344
|
-
}
|
|
345
|
-
handleClose(code) {
|
|
346
|
-
this.handleReconnectionAttempt({ code });
|
|
294
|
+
this.scheduleReconnect({
|
|
295
|
+
code,
|
|
296
|
+
reason: "close",
|
|
297
|
+
preferResume: !nonResumableGatewayCloseCodes.has(code)
|
|
298
|
+
});
|
|
347
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Handles missing heartbeat acknowledgements by forcing a reconnect flow.
|
|
302
|
+
*/
|
|
348
303
|
handleZombieConnection() {
|
|
349
|
-
this.
|
|
304
|
+
this.monitor.recordZombieConnection();
|
|
305
|
+
this.scheduleReconnect({
|
|
306
|
+
reason: "zombie",
|
|
307
|
+
preferResume: true
|
|
308
|
+
});
|
|
309
|
+
this.reconnectWithSocketRestart();
|
|
350
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Compatibility wrapper that maps to reconnect opcode handling.
|
|
313
|
+
*/
|
|
351
314
|
handleReconnect() {
|
|
352
|
-
this.
|
|
315
|
+
this.handleReconnectOpcode();
|
|
353
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Returns whether session_id and sequence are both available for RESUME.
|
|
319
|
+
*/
|
|
354
320
|
canResume() {
|
|
355
321
|
return Boolean(this.state.sessionId && this.sequence !== null);
|
|
356
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Sends a RESUME payload using cached session_id and latest sequence.
|
|
325
|
+
*/
|
|
357
326
|
resume() {
|
|
358
|
-
if (!this.client || !this.state.sessionId || this.sequence === null)
|
|
327
|
+
if (!this.client || !this.state.sessionId || this.sequence === null) {
|
|
359
328
|
return;
|
|
329
|
+
}
|
|
360
330
|
const payload = createResumePayload({
|
|
361
331
|
token: this.client.options.token,
|
|
362
332
|
sessionId: this.state.sessionId,
|
|
@@ -364,33 +334,49 @@ export class GatewayPlugin extends Plugin {
|
|
|
364
334
|
});
|
|
365
335
|
this.send(payload, true);
|
|
366
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Sends a gateway payload with size and rate-limit safeguards.
|
|
339
|
+
*/
|
|
367
340
|
send(payload, skipRateLimit = false) {
|
|
368
|
-
if (this.ws
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
341
|
+
if (!this.ws || this.ws.readyState !== socketOpenState) {
|
|
342
|
+
throw new Error("Gateway websocket is not open");
|
|
343
|
+
}
|
|
344
|
+
const isEssentialEvent = payload.op === GatewayOpcodes.Heartbeat ||
|
|
345
|
+
payload.op === GatewayOpcodes.Identify ||
|
|
346
|
+
payload.op === GatewayOpcodes.Resume;
|
|
347
|
+
if (!skipRateLimit && !isEssentialEvent && !this.rateLimit.canSend()) {
|
|
348
|
+
throw new Error(`Gateway rate limit exceeded. ${this.rateLimit.getRemainingEvents()} events remaining. Reset in ${this.rateLimit.getResetTime()}ms`);
|
|
349
|
+
}
|
|
350
|
+
const encodedPayload = JSON.stringify(payload);
|
|
351
|
+
const payloadSize = typeof Buffer !== "undefined"
|
|
352
|
+
? Buffer.byteLength(encodedPayload, "utf8")
|
|
353
|
+
: new TextEncoder().encode(encodedPayload).byteLength;
|
|
354
|
+
if (payloadSize > 4096) {
|
|
355
|
+
throw new Error("Gateway payload exceeds 4096-byte Discord limit");
|
|
356
|
+
}
|
|
357
|
+
this.ws.send(encodedPayload);
|
|
358
|
+
this.monitor.recordMessageSent();
|
|
359
|
+
if (!isEssentialEvent) {
|
|
360
|
+
this.rateLimit.recordEvent();
|
|
361
|
+
}
|
|
362
|
+
if (payload.op === GatewayOpcodes.Heartbeat) {
|
|
363
|
+
this.monitor.recordHeartbeat();
|
|
384
364
|
}
|
|
385
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Sends an IDENTIFY payload for a fresh gateway session.
|
|
368
|
+
*/
|
|
386
369
|
identify() {
|
|
387
|
-
if (!this.client)
|
|
370
|
+
if (!this.client) {
|
|
388
371
|
return;
|
|
372
|
+
}
|
|
389
373
|
const payload = createIdentifyPayload({
|
|
390
374
|
token: this.client.options.token,
|
|
391
375
|
intents: this.options.intents,
|
|
392
376
|
properties: {
|
|
393
|
-
os: process
|
|
377
|
+
os: typeof process !== "undefined" && process?.platform
|
|
378
|
+
? process.platform
|
|
379
|
+
: "unknown",
|
|
394
380
|
browser: "@buape/carbon - https://carbon.buape.com",
|
|
395
381
|
device: "@buape/carbon - https://carbon.buape.com"
|
|
396
382
|
},
|
|
@@ -399,30 +385,25 @@ export class GatewayPlugin extends Plugin {
|
|
|
399
385
|
this.send(payload, true);
|
|
400
386
|
}
|
|
401
387
|
/**
|
|
402
|
-
*
|
|
403
|
-
* @param data Presence data to update
|
|
388
|
+
* Updates bot presence over the gateway connection.
|
|
404
389
|
*/
|
|
405
390
|
updatePresence(data) {
|
|
406
391
|
if (!this.isConnected) {
|
|
407
392
|
throw new Error("Gateway is not connected");
|
|
408
393
|
}
|
|
409
|
-
|
|
410
|
-
this.send(payload);
|
|
394
|
+
this.send(createUpdatePresencePayload(data));
|
|
411
395
|
}
|
|
412
396
|
/**
|
|
413
|
-
*
|
|
414
|
-
* @param data Voice state data to update
|
|
397
|
+
* Updates bot voice state for a guild over the gateway connection.
|
|
415
398
|
*/
|
|
416
399
|
updateVoiceState(data) {
|
|
417
400
|
if (!this.isConnected) {
|
|
418
401
|
throw new Error("Gateway is not connected");
|
|
419
402
|
}
|
|
420
|
-
|
|
421
|
-
this.send(payload);
|
|
403
|
+
this.send(createUpdateVoiceStatePayload(data));
|
|
422
404
|
}
|
|
423
405
|
/**
|
|
424
|
-
*
|
|
425
|
-
* @param data Guild members request data
|
|
406
|
+
* Requests guild members and validates required intents/options.
|
|
426
407
|
*/
|
|
427
408
|
requestGuildMembers(data) {
|
|
428
409
|
if (!this.isConnected) {
|
|
@@ -441,11 +422,10 @@ export class GatewayPlugin extends Plugin {
|
|
|
441
422
|
if (!data.query && data.query !== "" && !data.user_ids) {
|
|
442
423
|
throw new Error("Either 'query' or 'user_ids' field is required for requestGuildMembers");
|
|
443
424
|
}
|
|
444
|
-
|
|
445
|
-
this.send(payload);
|
|
425
|
+
this.send(createRequestGuildMembersPayload(data));
|
|
446
426
|
}
|
|
447
427
|
/**
|
|
448
|
-
*
|
|
428
|
+
* Returns the current outbound gateway rate-limit snapshot.
|
|
449
429
|
*/
|
|
450
430
|
getRateLimitStatus() {
|
|
451
431
|
return {
|
|
@@ -455,7 +435,7 @@ export class GatewayPlugin extends Plugin {
|
|
|
455
435
|
};
|
|
456
436
|
}
|
|
457
437
|
/**
|
|
458
|
-
*
|
|
438
|
+
* Returns helpers describing which intents are currently enabled.
|
|
459
439
|
*/
|
|
460
440
|
getIntentsInfo() {
|
|
461
441
|
return {
|
|
@@ -468,12 +448,343 @@ export class GatewayPlugin extends Plugin {
|
|
|
468
448
|
};
|
|
469
449
|
}
|
|
470
450
|
/**
|
|
471
|
-
*
|
|
472
|
-
* @param intent The intent to check
|
|
451
|
+
* Checks if a specific intent bit is enabled.
|
|
473
452
|
*/
|
|
474
453
|
hasIntent(intent) {
|
|
475
454
|
return (this.options.intents & intent) !== 0;
|
|
476
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Guards handlers from acting on stale websocket instances.
|
|
458
|
+
*/
|
|
459
|
+
isCurrentSocket(socket, generation) {
|
|
460
|
+
return this.ws === socket && this.socketGeneration === generation;
|
|
461
|
+
}
|
|
462
|
+
onSocketEvent(socket, event, handler) {
|
|
463
|
+
if (typeof socket.on === "function") {
|
|
464
|
+
socket.on(event, (...args) => {
|
|
465
|
+
if (args.length === 0) {
|
|
466
|
+
handler(undefined);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
handler(args.length === 1 ? args[0] : args);
|
|
470
|
+
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (typeof socket.addEventListener === "function") {
|
|
474
|
+
socket.addEventListener(event, (incoming) => {
|
|
475
|
+
handler(incoming);
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
throw new Error("WebSocket implementation does not support event listeners");
|
|
480
|
+
}
|
|
481
|
+
getMessageText(incoming) {
|
|
482
|
+
const payload = this.extractSocketPayload(incoming);
|
|
483
|
+
if (typeof payload === "string") {
|
|
484
|
+
return payload;
|
|
485
|
+
}
|
|
486
|
+
if (payload instanceof ArrayBuffer) {
|
|
487
|
+
return textDecoder.decode(new Uint8Array(payload));
|
|
488
|
+
}
|
|
489
|
+
if (ArrayBuffer.isView(payload)) {
|
|
490
|
+
return textDecoder.decode(new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength));
|
|
491
|
+
}
|
|
492
|
+
if (payload && typeof payload === "object" && "toString" in payload) {
|
|
493
|
+
const text = String(payload);
|
|
494
|
+
return text === "[object Object]" ? null : text;
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
extractSocketPayload(incoming) {
|
|
499
|
+
if (Array.isArray(incoming)) {
|
|
500
|
+
return incoming[0];
|
|
501
|
+
}
|
|
502
|
+
if (incoming && typeof incoming === "object" && "data" in incoming) {
|
|
503
|
+
return incoming.data;
|
|
504
|
+
}
|
|
505
|
+
return incoming;
|
|
506
|
+
}
|
|
507
|
+
getCloseCode(incoming) {
|
|
508
|
+
if (Array.isArray(incoming)) {
|
|
509
|
+
const [code] = incoming;
|
|
510
|
+
if (typeof code === "number") {
|
|
511
|
+
return code;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (incoming && typeof incoming === "object" && "code" in incoming) {
|
|
515
|
+
const code = incoming.code;
|
|
516
|
+
if (typeof code === "number") {
|
|
517
|
+
return code;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return 1000;
|
|
521
|
+
}
|
|
522
|
+
getSocketError(incoming) {
|
|
523
|
+
const payload = this.extractSocketPayload(incoming);
|
|
524
|
+
if (payload instanceof Error) {
|
|
525
|
+
return payload;
|
|
526
|
+
}
|
|
527
|
+
if (payload && typeof payload === "object" && "error" in payload) {
|
|
528
|
+
const nested = payload.error;
|
|
529
|
+
if (nested instanceof Error) {
|
|
530
|
+
return nested;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return new Error(typeof payload === "string"
|
|
534
|
+
? payload
|
|
535
|
+
: "Gateway socket emitted an unknown error");
|
|
536
|
+
}
|
|
537
|
+
closeSocketImmediately(socket) {
|
|
538
|
+
if (typeof socket.terminate === "function") {
|
|
539
|
+
socket.terminate();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
socket.close(1000, "Gateway reconnect");
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Processes HELLO, starts heartbeat scheduling, then sends RESUME or IDENTIFY.
|
|
546
|
+
*/
|
|
547
|
+
handleHello(data, generation) {
|
|
548
|
+
const heartbeatInterval = data?.heartbeat_interval;
|
|
549
|
+
if (typeof heartbeatInterval !== "number" || heartbeatInterval <= 0) {
|
|
550
|
+
this.monitor.recordError();
|
|
551
|
+
this.emitter.emit("error", new Error("Gateway HELLO missing heartbeat"));
|
|
552
|
+
this.handleZombieConnection();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
startHeartbeat(this, {
|
|
556
|
+
interval: heartbeatInterval,
|
|
557
|
+
reconnectCallback: () => {
|
|
558
|
+
if (this.socketGeneration !== generation) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.handleZombieConnection();
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
const shouldResume = this.nextConnectionShouldResume && this.canResume();
|
|
565
|
+
this.nextConnectionShouldResume = false;
|
|
566
|
+
try {
|
|
567
|
+
if (shouldResume) {
|
|
568
|
+
this.resume();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.identify();
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
this.handleZombieConnection();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Marks heartbeat acknowledged and updates rolling ping metrics.
|
|
579
|
+
*/
|
|
580
|
+
handleHeartbeatAck() {
|
|
581
|
+
this.lastHeartbeatAck = true;
|
|
582
|
+
this.monitor.recordHeartbeatAck();
|
|
583
|
+
const latency = this.monitor.getMetrics().latency;
|
|
584
|
+
if (latency > 0) {
|
|
585
|
+
this.pings.push(latency);
|
|
586
|
+
if (this.pings.length > 10) {
|
|
587
|
+
this.pings.shift();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Immediately sends a heartbeat in response to gateway heartbeat requests.
|
|
593
|
+
*/
|
|
594
|
+
sendHeartbeatNow() {
|
|
595
|
+
this.lastHeartbeatAck = false;
|
|
596
|
+
try {
|
|
597
|
+
this.send({
|
|
598
|
+
op: GatewayOpcodes.Heartbeat,
|
|
599
|
+
d: this.sequence
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
this.handleZombieConnection();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Processes dispatch events, session caching, and Carbon event forwarding.
|
|
608
|
+
*/
|
|
609
|
+
handleDispatch(payload) {
|
|
610
|
+
const type = payload.t;
|
|
611
|
+
if (!listenerEvents.has(type)) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (type === "READY") {
|
|
615
|
+
const readyData = payload.d;
|
|
616
|
+
this.state.sessionId = readyData.session_id;
|
|
617
|
+
this.state.resumeGatewayUrl = readyData.resume_gateway_url;
|
|
618
|
+
}
|
|
619
|
+
if (type === "READY" || type === "RESUMED") {
|
|
620
|
+
this.isConnected = true;
|
|
621
|
+
this.reconnectAttempts = 0;
|
|
622
|
+
this.consecutiveResumeFailures = 0;
|
|
623
|
+
}
|
|
624
|
+
if (!this.client) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (this.options.eventFilter && !this.options.eventFilter(type)) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (type === "READY") {
|
|
631
|
+
const readyData = payload.d;
|
|
632
|
+
readyData.guilds.forEach((guild) => {
|
|
633
|
+
this.babyCache.setGuild(guild.id, {
|
|
634
|
+
available: false,
|
|
635
|
+
lastEvent: Date.now()
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (type === "GUILD_CREATE") {
|
|
640
|
+
const guildCreateData = payload.d;
|
|
641
|
+
const existingGuild = this.babyCache.getGuild(guildCreateData.id);
|
|
642
|
+
if (existingGuild && !existingGuild.available) {
|
|
643
|
+
this.babyCache.setGuild(guildCreateData.id, {
|
|
644
|
+
available: true,
|
|
645
|
+
lastEvent: Date.now()
|
|
646
|
+
});
|
|
647
|
+
this.client.eventHandler.handleEvent({
|
|
648
|
+
...guildCreateData,
|
|
649
|
+
clientId: this.client.options.clientId
|
|
650
|
+
}, "GUILD_AVAILABLE");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (type === "GUILD_DELETE") {
|
|
655
|
+
const guildDeleteData = payload.d;
|
|
656
|
+
const existingGuild = this.babyCache.getGuild(guildDeleteData.id);
|
|
657
|
+
if (existingGuild?.available && guildDeleteData.unavailable) {
|
|
658
|
+
this.babyCache.setGuild(guildDeleteData.id, {
|
|
659
|
+
available: false,
|
|
660
|
+
lastEvent: Date.now()
|
|
661
|
+
});
|
|
662
|
+
this.client.eventHandler.handleEvent({
|
|
663
|
+
...guildDeleteData,
|
|
664
|
+
clientId: this.client.options.clientId
|
|
665
|
+
}, "GUILD_UNAVAILABLE");
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (!guildDeleteData.unavailable) {
|
|
669
|
+
this.babyCache.removeGuild(guildDeleteData.id);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
this.client.eventHandler.handleEvent({ ...payload.d, clientId: this.client.options.clientId }, type);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Handles INVALID_SESSION and schedules reconnect with Discord-compliant delay.
|
|
676
|
+
*/
|
|
677
|
+
handleInvalidSession(data) {
|
|
678
|
+
const isResumable = Boolean(data) && this.canResume();
|
|
679
|
+
if (!isResumable) {
|
|
680
|
+
this.resetSessionState();
|
|
681
|
+
}
|
|
682
|
+
const discordRequiredDelay = 1000 + Math.floor(Math.random() * 4000);
|
|
683
|
+
this.scheduleReconnect({
|
|
684
|
+
reason: "invalid-session",
|
|
685
|
+
preferResume: isResumable,
|
|
686
|
+
minDelayMs: discordRequiredDelay
|
|
687
|
+
});
|
|
688
|
+
this.reconnectWithSocketRestart();
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Handles RECONNECT opcode by scheduling reconnect with resume preference.
|
|
692
|
+
*/
|
|
693
|
+
handleReconnectOpcode() {
|
|
694
|
+
this.scheduleReconnect({
|
|
695
|
+
reason: "reconnect-opcode",
|
|
696
|
+
preferResume: true,
|
|
697
|
+
allowImmediateFirstAttempt: true
|
|
698
|
+
});
|
|
699
|
+
this.reconnectWithSocketRestart();
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Terminates the current socket after reconnect has been scheduled.
|
|
703
|
+
*/
|
|
704
|
+
reconnectWithSocketRestart() {
|
|
705
|
+
stopHeartbeat(this);
|
|
706
|
+
this.lastHeartbeatAck = true;
|
|
707
|
+
if (!this.ws) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
this.silentSocketClosures.add(this.ws);
|
|
711
|
+
this.closeSocketImmediately(this.ws);
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Schedules a single reconnect attempt with backoff and attempt limits.
|
|
715
|
+
*/
|
|
716
|
+
scheduleReconnect(options) {
|
|
717
|
+
if (!this.shouldReconnect || this.reconnectTimeout || this.isConnecting) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const maxAttempts = this.options.reconnect?.maxAttempts ?? reconnectDefaults.maxAttempts;
|
|
721
|
+
if (Number.isFinite(maxAttempts) && this.reconnectAttempts >= maxAttempts) {
|
|
722
|
+
this.shouldReconnect = false;
|
|
723
|
+
this.emitter.emit("error", new Error(`Max reconnect attempts (${maxAttempts}) reached${options.code ? ` after close code ${options.code}` : ""}`));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
let shouldResume = options.preferResume && this.canResume();
|
|
727
|
+
const resumeFailureThreshold = 3;
|
|
728
|
+
if (shouldResume &&
|
|
729
|
+
this.consecutiveResumeFailures >= resumeFailureThreshold) {
|
|
730
|
+
this.resetSessionState();
|
|
731
|
+
shouldResume = false;
|
|
732
|
+
this.emitter.emit("debug", `Gateway forcing fresh IDENTIFY after ${resumeFailureThreshold} failed resume attempts`);
|
|
733
|
+
}
|
|
734
|
+
const delay = this.computeReconnectDelay(options);
|
|
735
|
+
if (shouldResume) {
|
|
736
|
+
this.consecutiveResumeFailures++;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
this.consecutiveResumeFailures = 0;
|
|
740
|
+
}
|
|
741
|
+
this.reconnectAttempts++;
|
|
742
|
+
this.emitter.emit("debug", `Gateway reconnect scheduled in ${delay}ms (${options.reason}, resume=${String(shouldResume)})`);
|
|
743
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
744
|
+
this.reconnectTimeout = undefined;
|
|
745
|
+
this.connect(shouldResume);
|
|
746
|
+
}, delay);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Computes exponential reconnect delay with jitter and optional minimum delay.
|
|
750
|
+
*/
|
|
751
|
+
computeReconnectDelay(options) {
|
|
752
|
+
const baseDelay = this.options.reconnect?.baseDelay ?? reconnectDefaults.baseDelay;
|
|
753
|
+
const maxDelay = this.options.reconnect?.maxDelay ?? reconnectDefaults.maxDelay;
|
|
754
|
+
if (options.allowImmediateFirstAttempt &&
|
|
755
|
+
this.reconnectAttempts === 0 &&
|
|
756
|
+
(options.minDelayMs ?? 0) === 0) {
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
const exponentialDelay = Math.min(baseDelay * 2 ** this.reconnectAttempts, maxDelay);
|
|
760
|
+
const jitterFactor = 0.85 + Math.random() * 0.3;
|
|
761
|
+
const jitteredDelay = Math.floor(exponentialDelay * jitterFactor);
|
|
762
|
+
return Math.max(options.minDelayMs ?? 0, jitteredDelay);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Clears any pending reconnect timer.
|
|
766
|
+
*/
|
|
767
|
+
clearReconnectTimeout() {
|
|
768
|
+
if (!this.reconnectTimeout) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
clearTimeout(this.reconnectTimeout);
|
|
772
|
+
this.reconnectTimeout = undefined;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Clears resume-related session state after non-resumable failures.
|
|
776
|
+
*/
|
|
777
|
+
resetSessionState() {
|
|
778
|
+
this.state.sessionId = null;
|
|
779
|
+
this.state.resumeGatewayUrl = null;
|
|
780
|
+
this.state.sequence = null;
|
|
781
|
+
this.sequence = null;
|
|
782
|
+
this.consecutiveResumeFailures = 0;
|
|
783
|
+
this.pings = [];
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Ensures gateway URLs include v=10 and encoding=json query parameters.
|
|
787
|
+
*/
|
|
477
788
|
ensureGatewayParams(url) {
|
|
478
789
|
try {
|
|
479
790
|
const parsed = new URL(url);
|