@bakit/gateway 3.0.0 → 3.0.2
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/dist/cluster.cjs +25 -0
- package/dist/cluster.js +1 -1
- package/dist/index.cjs +647 -0
- package/dist/index.d.cts +203 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +23 -9
- package/package.json +14 -6
package/dist/cluster.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var gateway = require('@bakit/gateway');
|
|
4
|
+
|
|
5
|
+
// src/lib/internal/cluster.ts
|
|
6
|
+
var {
|
|
7
|
+
BAKIT_CLUSTER_ID,
|
|
8
|
+
BAKIT_DISCORD_TOKEN,
|
|
9
|
+
BAKIT_DISCORD_INTENTS,
|
|
10
|
+
BAKIT_DISCORD_GATEWAY_URL,
|
|
11
|
+
BAKIT_DISCORD_GATEWAY_VERSION,
|
|
12
|
+
BAKIT_CLUSTER_SHARD_TOTAL,
|
|
13
|
+
BAKIT_CLUSTER_SHARD_LIST
|
|
14
|
+
} = process.env, cluster = new gateway.Cluster(Number(BAKIT_CLUSTER_ID), {
|
|
15
|
+
token: BAKIT_DISCORD_TOKEN,
|
|
16
|
+
intents: Number(BAKIT_DISCORD_INTENTS),
|
|
17
|
+
total: Number(BAKIT_CLUSTER_SHARD_TOTAL),
|
|
18
|
+
shards: JSON.parse(BAKIT_CLUSTER_SHARD_LIST),
|
|
19
|
+
gateway: {
|
|
20
|
+
baseURL: BAKIT_DISCORD_GATEWAY_URL,
|
|
21
|
+
version: Number(BAKIT_DISCORD_GATEWAY_VERSION)
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
gateway.ClusterProcess.bindProcess(cluster);
|
|
25
|
+
cluster.spawn();
|
package/dist/cluster.js
CHANGED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var EventEmitter = require('events');
|
|
4
|
+
var zlib = require('zlib');
|
|
5
|
+
var util = require('util');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
var WebSocket = require('ws');
|
|
8
|
+
var v10 = require('discord-api-types/v10');
|
|
9
|
+
var child_process = require('child_process');
|
|
10
|
+
var path = require('path');
|
|
11
|
+
var url = require('url');
|
|
12
|
+
var utils = require('@bakit/utils');
|
|
13
|
+
var collection = require('@discordjs/collection');
|
|
14
|
+
var PQueue = require('p-queue');
|
|
15
|
+
var rest = require('@bakit/rest');
|
|
16
|
+
|
|
17
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
18
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
19
|
+
|
|
20
|
+
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
21
|
+
var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
|
|
22
|
+
var PQueue__default = /*#__PURE__*/_interopDefault(PQueue);
|
|
23
|
+
|
|
24
|
+
// src/lib/Shard.ts
|
|
25
|
+
var MIN_HEARTBEAT_INTERVAL = 1e3, MAX_HEARTBEAT_INTERVAL = 6e4, SAFE_HEARTBEAT_INTERVAL = 45e3, ShardState = /* @__PURE__ */ ((ShardState2) => (ShardState2[ShardState2.Idle = 0] = "Idle", ShardState2[ShardState2.Connecting = 1] = "Connecting", ShardState2[ShardState2.Ready = 2] = "Ready", ShardState2[ShardState2.Resuming = 3] = "Resuming", ShardState2[ShardState2.Disconnecting = 4] = "Disconnecting", ShardState2[ShardState2.Disconnected = 5] = "Disconnected", ShardState2))(ShardState || {}), ShardStrategy = /* @__PURE__ */ ((ShardStrategy2) => (ShardStrategy2[ShardStrategy2.Resume = 0] = "Resume", ShardStrategy2[ShardStrategy2.Reconnect = 1] = "Reconnect", ShardStrategy2[ShardStrategy2.Shutdown = 2] = "Shutdown", ShardStrategy2))(ShardStrategy || {}), Shard = class extends EventEmitter__default.default {
|
|
26
|
+
constructor(id, options) {
|
|
27
|
+
super();
|
|
28
|
+
this.id = id;
|
|
29
|
+
this.options = options;
|
|
30
|
+
}
|
|
31
|
+
#state = 0 /* Idle */;
|
|
32
|
+
#ws;
|
|
33
|
+
#inflater;
|
|
34
|
+
#textDecoder = new util.TextDecoder();
|
|
35
|
+
#decompressBuffer = [];
|
|
36
|
+
#sessionId;
|
|
37
|
+
#lastSequence;
|
|
38
|
+
#resumeGatewayURL;
|
|
39
|
+
#lastHeartbeatSent = -1;
|
|
40
|
+
#lastHeartbeatAck = -1;
|
|
41
|
+
#missedHeartbeats = 0;
|
|
42
|
+
#heartbeatInterval;
|
|
43
|
+
#heartbeatTimeout;
|
|
44
|
+
#reconnectTimeout;
|
|
45
|
+
#strategy;
|
|
46
|
+
get state() {
|
|
47
|
+
return this.#state;
|
|
48
|
+
}
|
|
49
|
+
get latency() {
|
|
50
|
+
return this.#lastHeartbeatSent === -1 || this.#lastHeartbeatAck === -1 ? -1 : this.#lastHeartbeatAck - this.#lastHeartbeatSent;
|
|
51
|
+
}
|
|
52
|
+
get resumable() {
|
|
53
|
+
let hasSessionId = this.#sessionId !== void 0, hasSequence = this.#lastSequence !== void 0;
|
|
54
|
+
return this.#strategy === 0 /* Resume */ && hasSequence && hasSessionId;
|
|
55
|
+
}
|
|
56
|
+
async connect() {
|
|
57
|
+
if (this.#state !== 0 /* Idle */ && this.#state !== 5 /* Disconnected */)
|
|
58
|
+
throw new Error("Shard already connecting or connected");
|
|
59
|
+
return new Promise((resolve2, reject) => {
|
|
60
|
+
let cleanup = () => {
|
|
61
|
+
this.off("error", onError), this.off("ready", onReady);
|
|
62
|
+
}, onReady = () => {
|
|
63
|
+
cleanup(), resolve2();
|
|
64
|
+
}, onError = (err) => {
|
|
65
|
+
cleanup(), reject(err);
|
|
66
|
+
};
|
|
67
|
+
this.once("ready", onReady), this.once("error", onError), this.#init();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
disconnect(code) {
|
|
71
|
+
return new Promise((resolve2) => {
|
|
72
|
+
if (this.#state = 4 /* Disconnecting */, this.#strategy = 2 /* Shutdown */, !this.#ws) {
|
|
73
|
+
resolve2();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.#ws.once("close", () => {
|
|
77
|
+
resolve2();
|
|
78
|
+
}), this.#ws.close(code);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
resume() {
|
|
82
|
+
this.resumable && (this.#state = 3 /* Resuming */, this.send({
|
|
83
|
+
op: v10.GatewayOpcodes.Resume,
|
|
84
|
+
d: {
|
|
85
|
+
token: this.options.token,
|
|
86
|
+
session_id: this.#sessionId,
|
|
87
|
+
seq: this.#lastSequence
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
identify() {
|
|
92
|
+
this.send({
|
|
93
|
+
op: v10.GatewayOpcodes.Identify,
|
|
94
|
+
d: {
|
|
95
|
+
token: this.options.token,
|
|
96
|
+
intents: Number(this.options.intents),
|
|
97
|
+
properties: {
|
|
98
|
+
os: process.platform,
|
|
99
|
+
browser: "bakit",
|
|
100
|
+
device: "bakit"
|
|
101
|
+
},
|
|
102
|
+
shard: [this.id, this.options.total]
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
send(payload) {
|
|
107
|
+
this.#ws?.readyState === WebSocket__default.default.OPEN && this.#ws.send(JSON.stringify(payload));
|
|
108
|
+
}
|
|
109
|
+
sendHeartbeat() {
|
|
110
|
+
if (this.#lastHeartbeatSent !== -1 && this.#lastHeartbeatAck < this.#lastHeartbeatSent ? this.#missedHeartbeats++ : this.#missedHeartbeats = 0, this.#missedHeartbeats >= 2) {
|
|
111
|
+
this.emit("debug", "Missed 2 heartbeats, reconnecting"), this.#ws?.terminate();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.send({
|
|
115
|
+
op: v10.GatewayOpcodes.Heartbeat,
|
|
116
|
+
d: this.#lastSequence ?? null
|
|
117
|
+
}), this.#lastHeartbeatSent = Date.now();
|
|
118
|
+
}
|
|
119
|
+
#init() {
|
|
120
|
+
this.#state = 1 /* Connecting */, this.#strategy ??= 1 /* Reconnect */;
|
|
121
|
+
let url = new URL(
|
|
122
|
+
this.#strategy === 0 /* Resume */ && this.#resumeGatewayURL ? this.#resumeGatewayURL : this.options.gateway.baseURL
|
|
123
|
+
);
|
|
124
|
+
url.searchParams.set("v", String(this.options.gateway.version)), url.searchParams.set("encoding", "json"), url.searchParams.set("compress", "zlib-stream"), this.#ws = new WebSocket__default.default(url, { perMessageDeflate: false }), this.#inflater = zlib.createInflate({ flush: zlib.constants.Z_SYNC_FLUSH }), this.#inflater.on("data", (chunk) => this.#onInflate(chunk)), this.#inflater.on("error", (err) => {
|
|
125
|
+
this.emit("error", err), this.#ws?.terminate();
|
|
126
|
+
}), this.#ws.on("message", (data) => this.#onMessage(data)), this.#ws.on("close", (code) => this.#onClose(code)), this.#ws.on("error", (err) => this.emit("error", err));
|
|
127
|
+
}
|
|
128
|
+
#onMessage(data) {
|
|
129
|
+
if (!this.#inflater) {
|
|
130
|
+
try {
|
|
131
|
+
let text = data.toString(), payload = JSON.parse(text);
|
|
132
|
+
this.#handlePayload(payload);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this.emit("error", error);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let buffer;
|
|
139
|
+
Buffer.isBuffer(data) ? buffer = data : Array.isArray(data) ? buffer = Buffer.concat(data) : data instanceof ArrayBuffer ? buffer = Buffer.from(data) : buffer = Buffer.from(String(data));
|
|
140
|
+
let hasSyncFlush = buffer.length >= 4 && buffer[buffer.length - 4] === 0 && buffer[buffer.length - 3] === 0 && buffer[buffer.length - 2] === 255 && buffer[buffer.length - 1] === 255;
|
|
141
|
+
this.#inflater.write(buffer, (writeError) => {
|
|
142
|
+
if (writeError) {
|
|
143
|
+
this.emit("error", writeError);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
hasSyncFlush && this.#inflater?.flush(zlib.constants.Z_SYNC_FLUSH);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
#onInflate(chunk) {
|
|
150
|
+
this.#decompressBuffer.push(chunk);
|
|
151
|
+
let fullBuffer = Buffer.concat(this.#decompressBuffer);
|
|
152
|
+
try {
|
|
153
|
+
let text = this.#textDecoder.decode(fullBuffer), payload = JSON.parse(text);
|
|
154
|
+
this.#handlePayload(payload), this.#decompressBuffer = [];
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof SyntaxError) {
|
|
157
|
+
let text = this.#textDecoder.decode(fullBuffer);
|
|
158
|
+
if (text.includes("{") && !isValidJSON(text))
|
|
159
|
+
return;
|
|
160
|
+
this.emit("error", error), this.#decompressBuffer = [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
#handlePayload(payload) {
|
|
165
|
+
switch (this.emit("raw", payload), payload.op) {
|
|
166
|
+
case v10.GatewayOpcodes.Dispatch: {
|
|
167
|
+
this.#handleDispatch(payload);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case v10.GatewayOpcodes.Hello: {
|
|
171
|
+
this.#startHeartbeat(payload.d.heartbeat_interval), this.resumable ? this.resume() : this.emit("needIdentify");
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case v10.GatewayOpcodes.Heartbeat: {
|
|
175
|
+
this.sendHeartbeat();
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case v10.GatewayOpcodes.HeartbeatAck: {
|
|
179
|
+
this.#lastHeartbeatAck = Date.now();
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case v10.GatewayOpcodes.InvalidSession: {
|
|
183
|
+
payload.d ? this.#strategy = 0 /* Resume */ : (this.#strategy = 1 /* Reconnect */, this.#sessionId = void 0, this.#lastSequence = void 0, this.#resumeGatewayURL = void 0), this.emit("debug", `Invalid session (resumable=${this.resumable})`), this.#ws?.terminate();
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case v10.GatewayOpcodes.Reconnect: {
|
|
187
|
+
this.#strategy = 0 /* Resume */, this.emit("debug", "Reconnecting to gateway"), this.#ws?.terminate();
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
#handleDispatch(payload) {
|
|
193
|
+
switch (this.#lastSequence = payload.s, this.emit("dispatch", payload), payload.t) {
|
|
194
|
+
case v10.GatewayDispatchEvents.Ready: {
|
|
195
|
+
let { d: data } = payload;
|
|
196
|
+
this.#state = 2 /* Ready */, this.#sessionId = data.session_id, this.#resumeGatewayURL = data.resume_gateway_url, this.emit("ready", data);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case v10.GatewayDispatchEvents.Resumed: {
|
|
200
|
+
this.#state = 2 /* Ready */, this.#strategy = void 0, this.emit("resume");
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
#onClose(code) {
|
|
206
|
+
if (this.#cleanup(), this.#state = 5 /* Disconnected */, this.emit("disconnect", code), this.#strategy === 2 /* Shutdown */) {
|
|
207
|
+
switch (code) {
|
|
208
|
+
case v10.GatewayCloseCodes.AuthenticationFailed:
|
|
209
|
+
this.emit("error", new Error("Invalid token provided"));
|
|
210
|
+
break;
|
|
211
|
+
case v10.GatewayCloseCodes.InvalidIntents:
|
|
212
|
+
this.emit("error", new Error("Invalid intents provided"));
|
|
213
|
+
break;
|
|
214
|
+
case v10.GatewayCloseCodes.DisallowedIntents:
|
|
215
|
+
this.emit("error", new Error("Disallowed intents provided"));
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
} else this.#strategy || (this.#strategy = this.#getStrategy(code));
|
|
220
|
+
(this.#strategy === 1 /* Reconnect */ || this.#strategy === 0 /* Resume */) && this.#scheduleReconnect();
|
|
221
|
+
}
|
|
222
|
+
#getStrategy(code) {
|
|
223
|
+
switch (code) {
|
|
224
|
+
case v10.GatewayCloseCodes.AuthenticationFailed:
|
|
225
|
+
case v10.GatewayCloseCodes.InvalidIntents:
|
|
226
|
+
case v10.GatewayCloseCodes.DisallowedIntents:
|
|
227
|
+
return 2 /* Shutdown */;
|
|
228
|
+
case v10.GatewayCloseCodes.InvalidSeq:
|
|
229
|
+
case v10.GatewayCloseCodes.SessionTimedOut:
|
|
230
|
+
return 1 /* Reconnect */;
|
|
231
|
+
default:
|
|
232
|
+
return 0 /* Resume */;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
#scheduleReconnect(delay = 1e3) {
|
|
236
|
+
this.#reconnectTimeout || (this.#reconnectTimeout = setTimeout(() => {
|
|
237
|
+
this.#reconnectTimeout = void 0, this.#state = 0 /* Idle */, this.#init();
|
|
238
|
+
}, delay));
|
|
239
|
+
}
|
|
240
|
+
#startHeartbeat(interval) {
|
|
241
|
+
this.#heartbeatInterval && (clearInterval(this.#heartbeatInterval), this.#heartbeatInterval = void 0), (interval < MIN_HEARTBEAT_INTERVAL || interval > MAX_HEARTBEAT_INTERVAL) && (interval = SAFE_HEARTBEAT_INTERVAL);
|
|
242
|
+
let jitter = crypto.randomInt(0, 10) / 100, firstDelay = Math.floor(interval * jitter);
|
|
243
|
+
this.emit("debug", `Starting heartbeat (interval=${interval}ms, jitter=${firstDelay}ms)`), this.#heartbeatTimeout = setTimeout(() => {
|
|
244
|
+
this.sendHeartbeat(), this.#heartbeatInterval = setInterval(() => this.sendHeartbeat(), interval);
|
|
245
|
+
}, firstDelay);
|
|
246
|
+
}
|
|
247
|
+
#cleanup() {
|
|
248
|
+
clearTimeout(this.#reconnectTimeout), clearInterval(this.#heartbeatInterval), clearTimeout(this.#heartbeatTimeout), this.#inflater?.destroy(), this.#inflater = void 0, this.#ws?.removeAllListeners(), this.#ws = void 0, this.#decompressBuffer = [], this.#missedHeartbeats = 0;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
function isValidJSON(str) {
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(str), !0;
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
var EVAL_TIMEOUT = 3e4, __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))), __dirname$1 = path.dirname(__filename$1);
|
|
259
|
+
function isDispatchPayload(payload) {
|
|
260
|
+
return payload.op === "dispatch";
|
|
261
|
+
}
|
|
262
|
+
function isIdentifyPayload(payload) {
|
|
263
|
+
return payload.op === "identify";
|
|
264
|
+
}
|
|
265
|
+
function isSendPayload(payload) {
|
|
266
|
+
return payload.op === "send";
|
|
267
|
+
}
|
|
268
|
+
function isEvalRequestPayload(payload) {
|
|
269
|
+
return payload.op === "eval";
|
|
270
|
+
}
|
|
271
|
+
function isEvalResponsePayload(payload) {
|
|
272
|
+
return payload.op === "evalResponse";
|
|
273
|
+
}
|
|
274
|
+
var ClusterProcess = class _ClusterProcess extends EventEmitter__default.default {
|
|
275
|
+
constructor(manager, id, options = {}) {
|
|
276
|
+
super();
|
|
277
|
+
this.manager = manager;
|
|
278
|
+
this.id = id;
|
|
279
|
+
this.setMaxListeners(0);
|
|
280
|
+
let entry = path.resolve(__dirname$1, utils.isCommonJS() ? "cluster.cjs" : "cluster.js");
|
|
281
|
+
this.process = child_process.fork(entry, {
|
|
282
|
+
env: options.env,
|
|
283
|
+
execArgv: options.execArgv,
|
|
284
|
+
stdio: ["inherit", "inherit", "inherit", "ipc"]
|
|
285
|
+
}), this.#bindProcessEvents();
|
|
286
|
+
}
|
|
287
|
+
process;
|
|
288
|
+
#pendingEvals = /* @__PURE__ */ new Map();
|
|
289
|
+
#shards = /* @__PURE__ */ new Set();
|
|
290
|
+
get shards() {
|
|
291
|
+
return new Set(this.#shards);
|
|
292
|
+
}
|
|
293
|
+
get killed() {
|
|
294
|
+
return this.process.killed || !this.process.connected;
|
|
295
|
+
}
|
|
296
|
+
kill(signal = "SIGTERM") {
|
|
297
|
+
if (!this.killed) {
|
|
298
|
+
for (let [nonce, pending] of this.#pendingEvals)
|
|
299
|
+
clearTimeout(pending.timeout), pending.reject(new Error(`Process killed before eval completed (nonce: ${nonce})`));
|
|
300
|
+
this.#pendingEvals.clear(), this.process.kill(signal);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async eval(fn, ctx) {
|
|
304
|
+
let nonce = crypto.randomUUID();
|
|
305
|
+
return new Promise((resolve2, reject) => {
|
|
306
|
+
let timeoutId = setTimeout(() => {
|
|
307
|
+
this.#pendingEvals.delete(nonce), reject(new Error(`Eval timed out after ${EVAL_TIMEOUT}ms`));
|
|
308
|
+
}, EVAL_TIMEOUT);
|
|
309
|
+
this.#pendingEvals.set(nonce, {
|
|
310
|
+
resolve: resolve2,
|
|
311
|
+
reject,
|
|
312
|
+
timeout: timeoutId
|
|
313
|
+
});
|
|
314
|
+
let context;
|
|
315
|
+
try {
|
|
316
|
+
context = JSON.stringify(ctx ?? null);
|
|
317
|
+
} catch {
|
|
318
|
+
reject(new Error("Eval context is not serializable"));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
let script = `(${fn.toString()})(cluster, ${context})`;
|
|
322
|
+
this.sendIPC({
|
|
323
|
+
op: "eval",
|
|
324
|
+
d: { nonce, script }
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
send(idOrPayload, payload) {
|
|
329
|
+
let hasShardId = typeof idOrPayload == "number" && payload !== void 0, shardId = hasShardId ? idOrPayload : void 0, data = hasShardId ? payload : idOrPayload;
|
|
330
|
+
this.sendIPC({
|
|
331
|
+
op: "send",
|
|
332
|
+
d: { shardId, data }
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
sendIPC(message) {
|
|
336
|
+
if (!(!this.process.connected || this.process.killed))
|
|
337
|
+
try {
|
|
338
|
+
this.process.send(message, void 0, void 0, (err) => {
|
|
339
|
+
err && this.emit("error", err);
|
|
340
|
+
});
|
|
341
|
+
} catch (err) {
|
|
342
|
+
this.emit("error", err);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
identifyShard(id) {
|
|
346
|
+
this.sendIPC({
|
|
347
|
+
op: "identify",
|
|
348
|
+
d: id
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
#bindProcessEvents() {
|
|
352
|
+
this.process.on("message", (message) => this.#handleIPC(message)), this.process.on("error", (err) => this.emit("error", err)), this.process.on("disconnect", () => this.emit("debug", "Process disconnected")), this.process.on("exit", (code) => {
|
|
353
|
+
for (let [nonce, pending] of this.#pendingEvals)
|
|
354
|
+
clearTimeout(pending.timeout), pending.reject(new Error(`Process exited (code: ${code}) before eval completed (nonce: ${nonce})`));
|
|
355
|
+
this.#pendingEvals.clear();
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
#handleIPC(message) {
|
|
359
|
+
if (this.#isValidPayload(message)) {
|
|
360
|
+
if (isDispatchPayload(message)) {
|
|
361
|
+
this.#handleDispatchPayload(message);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (isEvalResponsePayload(message)) {
|
|
365
|
+
this.#handleEvalResponse(message);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
#handleDispatchPayload(payload) {
|
|
371
|
+
payload.t === "shardAdd" && this.#shards.add(payload.d[0]), this.emit(payload.t, ...payload.d);
|
|
372
|
+
}
|
|
373
|
+
#handleEvalResponse(payload) {
|
|
374
|
+
let pending = this.#pendingEvals.get(payload.d.nonce);
|
|
375
|
+
if (!pending) {
|
|
376
|
+
this.emit("debug", `Received eval response for unknown nonce: ${payload.d.nonce}`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (pending.timeout && clearTimeout(pending.timeout), this.#pendingEvals.delete(payload.d.nonce), payload.d.success)
|
|
380
|
+
pending.resolve({
|
|
381
|
+
success: true,
|
|
382
|
+
data: payload.d.result,
|
|
383
|
+
cluster: this
|
|
384
|
+
});
|
|
385
|
+
else {
|
|
386
|
+
let error = new Error(payload.d.error ?? "Unknown eval error");
|
|
387
|
+
pending.resolve({
|
|
388
|
+
success: false,
|
|
389
|
+
error,
|
|
390
|
+
cluster: this
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
#isValidPayload(message) {
|
|
395
|
+
if (typeof message != "object" || message === null)
|
|
396
|
+
return false;
|
|
397
|
+
let payload = message;
|
|
398
|
+
return payload.op === "dispatch" || payload.op === "identify" || payload.op === "send" || payload.op === "eval" || payload.op === "evalResponse";
|
|
399
|
+
}
|
|
400
|
+
static bindProcess(cluster) {
|
|
401
|
+
let superEmit = cluster.emit.bind(cluster), safeSend = (message) => {
|
|
402
|
+
process.connected && process.send?.(message, void 0, void 0, (err) => {
|
|
403
|
+
err && cluster.emit("error", err);
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
cluster.emit = function(eventName, ...args) {
|
|
407
|
+
let result = superEmit(eventName, ...args);
|
|
408
|
+
return safeSend({
|
|
409
|
+
op: "dispatch",
|
|
410
|
+
t: eventName,
|
|
411
|
+
d: args
|
|
412
|
+
}), result;
|
|
413
|
+
};
|
|
414
|
+
let messageHandler = async (message) => {
|
|
415
|
+
if (isIdentifyPayload(message)) {
|
|
416
|
+
cluster.shards.get(message.d)?.identify();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (isSendPayload(message)) {
|
|
420
|
+
message.d.shardId !== void 0 ? cluster.send(message.d.shardId, message.d.data) : cluster.send(message.d.data);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (isEvalRequestPayload(message)) {
|
|
424
|
+
await _ClusterProcess.#handleEvalRequest(cluster, message, safeSend);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
process.on("message", messageHandler);
|
|
429
|
+
}
|
|
430
|
+
static async #handleEvalRequest(cluster, payload, safeSend) {
|
|
431
|
+
let { nonce, script } = payload.d, executeEval = async () => await new Function("cluster", `return ${script}`)(cluster), timeoutId;
|
|
432
|
+
try {
|
|
433
|
+
let evalPromise = executeEval(), timeoutPromise = new Promise((_, reject) => {
|
|
434
|
+
timeoutId = setTimeout(() => {
|
|
435
|
+
reject(new Error(`Eval execution timed out after ${EVAL_TIMEOUT}ms`));
|
|
436
|
+
}, EVAL_TIMEOUT);
|
|
437
|
+
}), result = await Promise.race([evalPromise, timeoutPromise]);
|
|
438
|
+
timeoutId && clearTimeout(timeoutId), safeSend({
|
|
439
|
+
op: "evalResponse",
|
|
440
|
+
d: {
|
|
441
|
+
nonce,
|
|
442
|
+
success: !0,
|
|
443
|
+
result
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
} catch (err) {
|
|
447
|
+
timeoutId && clearTimeout(timeoutId), safeSend({
|
|
448
|
+
op: "evalResponse",
|
|
449
|
+
d: {
|
|
450
|
+
nonce,
|
|
451
|
+
success: false,
|
|
452
|
+
result: void 0,
|
|
453
|
+
error: err instanceof Error ? err.message : String(err)
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var Cluster = class extends EventEmitter__default.default {
|
|
460
|
+
constructor(id, options) {
|
|
461
|
+
super();
|
|
462
|
+
this.id = id;
|
|
463
|
+
this.options = options;
|
|
464
|
+
}
|
|
465
|
+
shards = new collection.Collection();
|
|
466
|
+
#readyCount = 0;
|
|
467
|
+
#starting = false;
|
|
468
|
+
get size() {
|
|
469
|
+
return this.shards.size;
|
|
470
|
+
}
|
|
471
|
+
get ready() {
|
|
472
|
+
return this.#readyCount === this.options.shards.length;
|
|
473
|
+
}
|
|
474
|
+
async spawn() {
|
|
475
|
+
if (!this.#starting) {
|
|
476
|
+
this.#starting = true, this.emit("debug", `Spawning ${this.options.shards.length} shards...`);
|
|
477
|
+
for (let i of this.options.shards)
|
|
478
|
+
await this.#spawnShard(i);
|
|
479
|
+
this.emit("debug", "All shards spawned");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async shutdown(code = 1e3) {
|
|
483
|
+
this.emit("debug", "Shutting down cluster...");
|
|
484
|
+
let tasks = [];
|
|
485
|
+
for (let shard of this.shards.values())
|
|
486
|
+
tasks.push(shard.disconnect(code));
|
|
487
|
+
await Promise.allSettled(tasks), this.emit("debug", "Cluster shutdown complete");
|
|
488
|
+
}
|
|
489
|
+
broadcast(fn) {
|
|
490
|
+
for (let shard of this.shards.values())
|
|
491
|
+
fn(shard);
|
|
492
|
+
}
|
|
493
|
+
send(idOrPayload, payload) {
|
|
494
|
+
let hasId = typeof idOrPayload == "number" && payload !== void 0, shardId = hasId ? idOrPayload : void 0, data = hasId ? payload : idOrPayload;
|
|
495
|
+
shardId !== void 0 ? this.shards.get(shardId)?.send(data) : this.broadcast((shard) => shard.send(data));
|
|
496
|
+
}
|
|
497
|
+
async #spawnShard(id) {
|
|
498
|
+
let shard = new Shard(id, {
|
|
499
|
+
token: this.options.token,
|
|
500
|
+
intents: this.options.intents,
|
|
501
|
+
total: this.options.total,
|
|
502
|
+
gateway: this.options.gateway
|
|
503
|
+
});
|
|
504
|
+
this.#bindShardEvents(shard), this.shards.set(id, shard), this.emit("shardAdd", id), await shard.connect();
|
|
505
|
+
}
|
|
506
|
+
#bindShardEvents(shard) {
|
|
507
|
+
let id = shard.id;
|
|
508
|
+
shard.on("ready", () => {
|
|
509
|
+
this.#readyCount++, this.emit("debug", `Shard ${id} ready`), this.emit("shardReady", id), this.ready && this.emit("ready");
|
|
510
|
+
}), shard.on("resume", () => {
|
|
511
|
+
this.emit("debug", `Shard ${id} resumed`), this.emit("shardResume", id);
|
|
512
|
+
}), shard.on("disconnect", (code) => {
|
|
513
|
+
this.emit("debug", `Shard ${id} disconnected (${code})`), this.emit("shardDisconnect", id, code);
|
|
514
|
+
}), shard.on("error", (err) => {
|
|
515
|
+
this.emit("debug", `Shard ${id} error: ${err.message}`), this.emit("shardError", id, err);
|
|
516
|
+
}), shard.on("raw", (payload) => {
|
|
517
|
+
this.emit("raw", id, payload);
|
|
518
|
+
}), shard.on("dispatch", (payload) => {
|
|
519
|
+
this.emit("dispatch", id, payload);
|
|
520
|
+
}), shard.on("needIdentify", () => {
|
|
521
|
+
this.emit("needIdentify", id);
|
|
522
|
+
}), shard.on("debug", (msg) => {
|
|
523
|
+
this.emit("debug", `[Shard ${id}] ${msg}`);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
var ShardingManager = class extends EventEmitter__default.default {
|
|
528
|
+
clusters = new collection.Collection();
|
|
529
|
+
options;
|
|
530
|
+
rest;
|
|
531
|
+
#gatewayInfo;
|
|
532
|
+
#totalShards = 0;
|
|
533
|
+
#readyCount = 0;
|
|
534
|
+
#identifyQueue;
|
|
535
|
+
constructor(options, rest$1) {
|
|
536
|
+
super(), this.setMaxListeners(0), this.options = {
|
|
537
|
+
shardsPerCluster: 5,
|
|
538
|
+
totalShards: options.totalShards ?? "auto",
|
|
539
|
+
...options
|
|
540
|
+
}, rest$1 || (rest$1 = new rest.REST({ token: this.options.token })), this.rest = rest$1;
|
|
541
|
+
}
|
|
542
|
+
get totalClusters() {
|
|
543
|
+
let { shardsPerCluster } = this.options;
|
|
544
|
+
return shardsPerCluster <= 0 ? 0 : Math.ceil(this.totalShards / shardsPerCluster);
|
|
545
|
+
}
|
|
546
|
+
get totalShards() {
|
|
547
|
+
return this.#totalShards;
|
|
548
|
+
}
|
|
549
|
+
get ready() {
|
|
550
|
+
return this.totalClusters > 0 && this.#readyCount === this.totalClusters;
|
|
551
|
+
}
|
|
552
|
+
async spawn() {
|
|
553
|
+
this.#gatewayInfo = await this.rest.get("/gateway/bot");
|
|
554
|
+
let { session_start_limit: limit } = this.#gatewayInfo;
|
|
555
|
+
this.#totalShards = typeof this.options.totalShards == "number" ? this.options.totalShards : this.#gatewayInfo.shards, this.#identifyQueue = new PQueue__default.default({
|
|
556
|
+
concurrency: limit.max_concurrency,
|
|
557
|
+
intervalCap: limit.max_concurrency,
|
|
558
|
+
interval: 5e3
|
|
559
|
+
});
|
|
560
|
+
let { totalShards, totalClusters } = this;
|
|
561
|
+
this.emit("debug", `Spawning ${totalClusters} clusters (${totalShards} total shards)...`);
|
|
562
|
+
for (let i = 0; i < totalClusters; i++)
|
|
563
|
+
this.#spawnCluster(i);
|
|
564
|
+
this.emit("debug", "All clusters spawned");
|
|
565
|
+
}
|
|
566
|
+
async kill(signal = "SIGTERM") {
|
|
567
|
+
this.emit("debug", "Shutting down all clusters...");
|
|
568
|
+
let tasks = [];
|
|
569
|
+
for (let cluster of this.clusters.values())
|
|
570
|
+
tasks.push(
|
|
571
|
+
new Promise((resolve2) => {
|
|
572
|
+
cluster.process.once("exit", () => resolve2()), cluster.kill(signal);
|
|
573
|
+
})
|
|
574
|
+
);
|
|
575
|
+
await Promise.all(tasks), this.emit("debug", "All clusters shut down");
|
|
576
|
+
}
|
|
577
|
+
broadcast(payload) {
|
|
578
|
+
for (let cluster of this.clusters.values())
|
|
579
|
+
cluster.send(payload);
|
|
580
|
+
}
|
|
581
|
+
async broadcastEval(fn) {
|
|
582
|
+
let promises = this.clusters.map((cluster) => cluster.eval(fn));
|
|
583
|
+
return Promise.all(promises);
|
|
584
|
+
}
|
|
585
|
+
send(shardId, payload) {
|
|
586
|
+
let cluster = this.clusters.find((cluster2) => cluster2.shards.has(shardId));
|
|
587
|
+
if (!cluster)
|
|
588
|
+
throw new Error(`Shard ${shardId} not found`);
|
|
589
|
+
cluster.send(payload);
|
|
590
|
+
}
|
|
591
|
+
requestIdentify(cluster, shardId) {
|
|
592
|
+
this.#identifyQueue?.add(() => cluster.identifyShard(shardId));
|
|
593
|
+
}
|
|
594
|
+
#getShardIdsForCluster(clusterId) {
|
|
595
|
+
let start = clusterId * this.options.shardsPerCluster, end = Math.min(start + this.options.shardsPerCluster, this.totalShards);
|
|
596
|
+
return Array.from({ length: end - start }, (_, i) => start + i);
|
|
597
|
+
}
|
|
598
|
+
#spawnCluster(id) {
|
|
599
|
+
let shardIds = this.#getShardIdsForCluster(id), firstShardId = shardIds[0], lastShardId = shardIds[shardIds.length - 1];
|
|
600
|
+
this.emit("debug", `Spawning cluster ${id} (shards ${firstShardId}-${lastShardId})`);
|
|
601
|
+
let env = {
|
|
602
|
+
...process.env,
|
|
603
|
+
BAKIT_CLUSTER_ID: String(id),
|
|
604
|
+
BAKIT_CLUSTER_SHARD_TOTAL: String(this.totalShards),
|
|
605
|
+
BAKIT_CLUSTER_SHARD_LIST: JSON.stringify(shardIds),
|
|
606
|
+
BAKIT_DISCORD_TOKEN: this.options.token,
|
|
607
|
+
BAKIT_DISCORD_INTENTS: String(this.options.intents),
|
|
608
|
+
BAKIT_DISCORD_GATEWAY_URL: this.#gatewayInfo?.url,
|
|
609
|
+
BAKIT_DISCORD_GATEWAY_VERSION: "10"
|
|
610
|
+
}, cluster = new ClusterProcess(this, id, { env });
|
|
611
|
+
this.#bindClusterEvents(cluster, id), this.clusters.set(id, cluster), this.emit("clusterCreate", cluster);
|
|
612
|
+
}
|
|
613
|
+
#bindClusterEvents(cluster, id) {
|
|
614
|
+
cluster.on("ready", () => {
|
|
615
|
+
this.#readyCount++, this.emit("clusterReady", cluster), this.ready && this.emit("ready");
|
|
616
|
+
}), cluster.process.on("exit", (code) => {
|
|
617
|
+
this.emit("clusterExit", cluster, code), this.clusters.delete(id), this.#readyCount = Math.max(0, this.#readyCount - 1);
|
|
618
|
+
}), cluster.on("error", (err) => {
|
|
619
|
+
this.emit("clusterError", cluster, err);
|
|
620
|
+
}), cluster.on("debug", (msg) => {
|
|
621
|
+
this.emit("debug", `[Cluster ${id}] ${msg}`);
|
|
622
|
+
}), cluster.on("dispatch", (shardId, payload) => {
|
|
623
|
+
this.emit("dispatch", cluster, shardId, payload);
|
|
624
|
+
}), cluster.on("raw", (shardId, payload) => {
|
|
625
|
+
this.emit("raw", cluster, shardId, payload);
|
|
626
|
+
}), cluster.on("shardAdd", (shardId) => {
|
|
627
|
+
this.emit("shardAdd", cluster, shardId);
|
|
628
|
+
}), cluster.on("shardReady", (shardId) => {
|
|
629
|
+
this.emit("shardReady", cluster, shardId);
|
|
630
|
+
}), cluster.on("shardDisconnect", (shardId, code) => {
|
|
631
|
+
this.emit("shardDisconnect", cluster, shardId, code);
|
|
632
|
+
}), cluster.on("shardResume", (shardId) => {
|
|
633
|
+
this.emit("shardResume", cluster, shardId);
|
|
634
|
+
}), cluster.on("shardError", (shardId, error) => {
|
|
635
|
+
this.emit("shardError", cluster, shardId, error);
|
|
636
|
+
}), cluster.on("needIdentify", (shardId) => {
|
|
637
|
+
this.requestIdentify(cluster, shardId);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
exports.Cluster = Cluster;
|
|
643
|
+
exports.ClusterProcess = ClusterProcess;
|
|
644
|
+
exports.Shard = Shard;
|
|
645
|
+
exports.ShardState = ShardState;
|
|
646
|
+
exports.ShardStrategy = ShardStrategy;
|
|
647
|
+
exports.ShardingManager = ShardingManager;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
import { GatewayReadyDispatchData, GatewayReceivePayload, GatewayDispatchPayload, GatewaySendPayload } from 'discord-api-types/v10';
|
|
3
|
+
import { ChildProcess } from 'node:child_process';
|
|
4
|
+
import { Collection } from '@discordjs/collection';
|
|
5
|
+
import { RESTLike } from '@bakit/rest';
|
|
6
|
+
|
|
7
|
+
declare enum ShardState {
|
|
8
|
+
Idle = 0,
|
|
9
|
+
Connecting = 1,
|
|
10
|
+
Ready = 2,
|
|
11
|
+
Resuming = 3,
|
|
12
|
+
Disconnecting = 4,
|
|
13
|
+
Disconnected = 5
|
|
14
|
+
}
|
|
15
|
+
declare enum ShardStrategy {
|
|
16
|
+
Resume = 0,
|
|
17
|
+
Reconnect = 1,
|
|
18
|
+
Shutdown = 2
|
|
19
|
+
}
|
|
20
|
+
interface ShardOptions {
|
|
21
|
+
total: number;
|
|
22
|
+
token: string;
|
|
23
|
+
intents: number | bigint;
|
|
24
|
+
gateway: {
|
|
25
|
+
baseURL: string;
|
|
26
|
+
version: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface ShardEvents {
|
|
30
|
+
ready: [data: GatewayReadyDispatchData];
|
|
31
|
+
disconnect: [code: number];
|
|
32
|
+
resume: [];
|
|
33
|
+
error: [error: Error];
|
|
34
|
+
debug: [message: string];
|
|
35
|
+
raw: [payload: GatewayReceivePayload];
|
|
36
|
+
dispatch: [payload: GatewayDispatchPayload];
|
|
37
|
+
needIdentify: [];
|
|
38
|
+
}
|
|
39
|
+
declare class Shard extends EventEmitter<ShardEvents> {
|
|
40
|
+
#private;
|
|
41
|
+
readonly id: number;
|
|
42
|
+
readonly options: ShardOptions;
|
|
43
|
+
constructor(id: number, options: ShardOptions);
|
|
44
|
+
get state(): ShardState;
|
|
45
|
+
get latency(): number;
|
|
46
|
+
get resumable(): boolean;
|
|
47
|
+
connect(): Promise<void>;
|
|
48
|
+
disconnect(code: number): Promise<void>;
|
|
49
|
+
resume(): void;
|
|
50
|
+
identify(): void;
|
|
51
|
+
send(payload: GatewaySendPayload): void;
|
|
52
|
+
sendHeartbeat(): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ClusterOptions {
|
|
56
|
+
token: string;
|
|
57
|
+
intents: bigint | number;
|
|
58
|
+
shards: number[];
|
|
59
|
+
total: number;
|
|
60
|
+
gateway: {
|
|
61
|
+
baseURL: string;
|
|
62
|
+
version: number;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
interface ClusterEvents {
|
|
66
|
+
shardAdd: [id: number];
|
|
67
|
+
shardReady: [id: number];
|
|
68
|
+
shardDisconnect: [id: number, code: number];
|
|
69
|
+
shardResume: [id: number];
|
|
70
|
+
shardError: [id: number, error: Error];
|
|
71
|
+
needIdentify: [id: number];
|
|
72
|
+
debug: [message: string];
|
|
73
|
+
dispatch: [shardId: number, payload: GatewayDispatchPayload];
|
|
74
|
+
raw: [shardId: number, payload: GatewayReceivePayload];
|
|
75
|
+
ready: [];
|
|
76
|
+
error: [error: Error];
|
|
77
|
+
}
|
|
78
|
+
declare class Cluster extends EventEmitter<ClusterEvents> {
|
|
79
|
+
#private;
|
|
80
|
+
readonly id: number;
|
|
81
|
+
readonly options: ClusterOptions;
|
|
82
|
+
readonly shards: Collection<number, Shard>;
|
|
83
|
+
constructor(id: number, options: ClusterOptions);
|
|
84
|
+
get size(): number;
|
|
85
|
+
get ready(): boolean;
|
|
86
|
+
spawn(): Promise<void>;
|
|
87
|
+
shutdown(code?: number): Promise<void>;
|
|
88
|
+
broadcast(fn: (shard: Shard) => void): void;
|
|
89
|
+
send(payload: GatewaySendPayload): void;
|
|
90
|
+
send(shardId: number, payload: GatewaySendPayload): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface ShardingManagerOptions {
|
|
94
|
+
token: string;
|
|
95
|
+
intents: bigint | number;
|
|
96
|
+
totalShards?: number | "auto";
|
|
97
|
+
shardsPerCluster?: number;
|
|
98
|
+
}
|
|
99
|
+
interface ShardingManagerEvents {
|
|
100
|
+
shardAdd: [cluster: ClusterProcess, shardId: number];
|
|
101
|
+
shardReady: [cluster: ClusterProcess, shardId: number];
|
|
102
|
+
shardDisconnect: [cluster: ClusterProcess, shardId: number, code: number];
|
|
103
|
+
shardResume: [cluster: ClusterProcess, shardId: number];
|
|
104
|
+
shardError: [cluster: ClusterProcess, shardId: number, error: Error];
|
|
105
|
+
clusterCreate: [cluster: ClusterProcess];
|
|
106
|
+
clusterReady: [cluster: ClusterProcess];
|
|
107
|
+
clusterExit: [cluster: ClusterProcess, code: number | null];
|
|
108
|
+
clusterError: [cluster: ClusterProcess, error: Error];
|
|
109
|
+
dispatch: [cluster: ClusterProcess, shardId: number, payload: GatewayDispatchPayload];
|
|
110
|
+
raw: [cluster: ClusterProcess, shardId: number, payload: GatewayReceivePayload];
|
|
111
|
+
debug: [message: string];
|
|
112
|
+
ready: [];
|
|
113
|
+
}
|
|
114
|
+
declare class ShardingManager extends EventEmitter<ShardingManagerEvents> {
|
|
115
|
+
#private;
|
|
116
|
+
readonly clusters: Collection<number, ClusterProcess>;
|
|
117
|
+
readonly options: Required<ShardingManagerOptions>;
|
|
118
|
+
readonly rest: RESTLike;
|
|
119
|
+
constructor(options: ShardingManagerOptions, rest?: RESTLike);
|
|
120
|
+
get totalClusters(): number;
|
|
121
|
+
get totalShards(): number;
|
|
122
|
+
get ready(): boolean;
|
|
123
|
+
spawn(): Promise<void>;
|
|
124
|
+
kill(signal?: NodeJS.Signals): Promise<void>;
|
|
125
|
+
broadcast(payload: GatewaySendPayload): void;
|
|
126
|
+
broadcastEval<T>(fn: (cluster: Cluster) => T | Promise<T>): Promise<EvalResult<T>[]>;
|
|
127
|
+
send(shardId: number, payload: GatewaySendPayload): void;
|
|
128
|
+
protected requestIdentify(cluster: ClusterProcess, shardId: number): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type ClusterIPCDispatchPayload<E extends keyof ClusterEvents = keyof ClusterEvents> = {
|
|
132
|
+
op: "dispatch";
|
|
133
|
+
t: E;
|
|
134
|
+
d: ClusterEvents[E];
|
|
135
|
+
};
|
|
136
|
+
interface ClusterIPCIdentifyPayload {
|
|
137
|
+
op: "identify";
|
|
138
|
+
d: number;
|
|
139
|
+
}
|
|
140
|
+
interface ClusterIPCGatewaySendPayload {
|
|
141
|
+
op: "send";
|
|
142
|
+
d: {
|
|
143
|
+
shardId?: number;
|
|
144
|
+
data: GatewaySendPayload;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
interface ClusterIPCEvalRequestPayload {
|
|
148
|
+
op: "eval";
|
|
149
|
+
d: {
|
|
150
|
+
nonce: string;
|
|
151
|
+
script: string;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
interface ClusterIPCEvalResponsePayload {
|
|
155
|
+
op: "evalResponse";
|
|
156
|
+
d: {
|
|
157
|
+
nonce: string;
|
|
158
|
+
success: boolean;
|
|
159
|
+
result: unknown;
|
|
160
|
+
error?: string;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
type ClusterIPCPayload = ClusterIPCDispatchPayload | ClusterIPCIdentifyPayload | ClusterIPCGatewaySendPayload | ClusterIPCEvalRequestPayload | ClusterIPCEvalResponsePayload;
|
|
164
|
+
interface EvalResult<T> {
|
|
165
|
+
/**
|
|
166
|
+
* Whether the evaluation was successful
|
|
167
|
+
*/
|
|
168
|
+
success: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* The result of the evaluation if successful
|
|
171
|
+
*/
|
|
172
|
+
data?: T;
|
|
173
|
+
/**
|
|
174
|
+
* The error if evaluation failed
|
|
175
|
+
*/
|
|
176
|
+
error?: Error;
|
|
177
|
+
/**
|
|
178
|
+
* The cluster process that executed the evaluation
|
|
179
|
+
*/
|
|
180
|
+
cluster: ClusterProcess;
|
|
181
|
+
}
|
|
182
|
+
interface ClusterProcessOptions {
|
|
183
|
+
env?: NodeJS.ProcessEnv;
|
|
184
|
+
execArgv?: string[];
|
|
185
|
+
}
|
|
186
|
+
declare class ClusterProcess extends EventEmitter<ClusterEvents> {
|
|
187
|
+
#private;
|
|
188
|
+
readonly manager: ShardingManager;
|
|
189
|
+
readonly id: number;
|
|
190
|
+
readonly process: ChildProcess;
|
|
191
|
+
constructor(manager: ShardingManager, id: number, options?: ClusterProcessOptions);
|
|
192
|
+
get shards(): Set<number>;
|
|
193
|
+
get killed(): boolean;
|
|
194
|
+
kill(signal?: NodeJS.Signals): void;
|
|
195
|
+
eval<T, C = unknown>(fn: (cluster: Cluster, ctx: C) => T | Promise<T>, ctx?: C): Promise<EvalResult<T>>;
|
|
196
|
+
send(payload: GatewaySendPayload): void;
|
|
197
|
+
send(shardId: number, payload: GatewaySendPayload): void;
|
|
198
|
+
sendIPC(message: ClusterIPCPayload): void;
|
|
199
|
+
identifyShard(id: number): void;
|
|
200
|
+
static bindProcess(cluster: Cluster): void;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { Cluster, type ClusterEvents, type ClusterIPCDispatchPayload, type ClusterIPCEvalRequestPayload, type ClusterIPCEvalResponsePayload, type ClusterIPCGatewaySendPayload, type ClusterIPCIdentifyPayload, type ClusterIPCPayload, type ClusterOptions, ClusterProcess, type ClusterProcessOptions, type EvalResult, Shard, type ShardEvents, type ShardOptions, ShardState, ShardStrategy, ShardingManager, type ShardingManagerEvents, type ShardingManagerOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
2
|
import { GatewayReadyDispatchData, GatewayReceivePayload, GatewayDispatchPayload, GatewaySendPayload } from 'discord-api-types/v10';
|
|
3
3
|
import { ChildProcess } from 'node:child_process';
|
|
4
|
-
import { Collection } from '@
|
|
5
|
-
import {
|
|
4
|
+
import { Collection } from '@discordjs/collection';
|
|
5
|
+
import { RESTLike } from '@bakit/rest';
|
|
6
6
|
|
|
7
7
|
declare enum ShardState {
|
|
8
8
|
Idle = 0,
|
|
@@ -115,8 +115,8 @@ declare class ShardingManager extends EventEmitter<ShardingManagerEvents> {
|
|
|
115
115
|
#private;
|
|
116
116
|
readonly clusters: Collection<number, ClusterProcess>;
|
|
117
117
|
readonly options: Required<ShardingManagerOptions>;
|
|
118
|
-
readonly rest:
|
|
119
|
-
constructor(options: ShardingManagerOptions, rest?:
|
|
118
|
+
readonly rest: RESTLike;
|
|
119
|
+
constructor(options: ShardingManagerOptions, rest?: RESTLike);
|
|
120
120
|
get totalClusters(): number;
|
|
121
121
|
get totalShards(): number;
|
|
122
122
|
get ready(): boolean;
|
|
@@ -124,6 +124,7 @@ declare class ShardingManager extends EventEmitter<ShardingManagerEvents> {
|
|
|
124
124
|
kill(signal?: NodeJS.Signals): Promise<void>;
|
|
125
125
|
broadcast(payload: GatewaySendPayload): void;
|
|
126
126
|
broadcastEval<T>(fn: (cluster: Cluster) => T | Promise<T>): Promise<EvalResult<T>[]>;
|
|
127
|
+
send(shardId: number, payload: GatewaySendPayload): void;
|
|
127
128
|
protected requestIdentify(cluster: ClusterProcess, shardId: number): void;
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -188,6 +189,7 @@ declare class ClusterProcess extends EventEmitter<ClusterEvents> {
|
|
|
188
189
|
readonly id: number;
|
|
189
190
|
readonly process: ChildProcess;
|
|
190
191
|
constructor(manager: ShardingManager, id: number, options?: ClusterProcessOptions);
|
|
192
|
+
get shards(): Set<number>;
|
|
191
193
|
get killed(): boolean;
|
|
192
194
|
kill(signal?: NodeJS.Signals): void;
|
|
193
195
|
eval<T, C = unknown>(fn: (cluster: Cluster, ctx: C) => T | Promise<T>, ctx?: C): Promise<EvalResult<T>>;
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,9 @@ import { GatewayOpcodes, GatewayDispatchEvents, GatewayCloseCodes } from 'discor
|
|
|
7
7
|
import { fork } from 'child_process';
|
|
8
8
|
import { dirname, resolve } from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
-
import {
|
|
10
|
+
import { isCommonJS } from '@bakit/utils';
|
|
11
|
+
import { Collection } from '@discordjs/collection';
|
|
12
|
+
import PQueue from 'p-queue';
|
|
11
13
|
import { REST } from '@bakit/rest';
|
|
12
14
|
|
|
13
15
|
// src/lib/Shard.ts
|
|
@@ -266,7 +268,7 @@ var ClusterProcess = class _ClusterProcess extends EventEmitter {
|
|
|
266
268
|
this.manager = manager;
|
|
267
269
|
this.id = id;
|
|
268
270
|
this.setMaxListeners(0);
|
|
269
|
-
let entry = resolve(__dirname$1, "cluster.js");
|
|
271
|
+
let entry = resolve(__dirname$1, isCommonJS() ? "cluster.cjs" : "cluster.js");
|
|
270
272
|
this.process = fork(entry, {
|
|
271
273
|
env: options.env,
|
|
272
274
|
execArgv: options.execArgv,
|
|
@@ -275,6 +277,10 @@ var ClusterProcess = class _ClusterProcess extends EventEmitter {
|
|
|
275
277
|
}
|
|
276
278
|
process;
|
|
277
279
|
#pendingEvals = /* @__PURE__ */ new Map();
|
|
280
|
+
#shards = /* @__PURE__ */ new Set();
|
|
281
|
+
get shards() {
|
|
282
|
+
return new Set(this.#shards);
|
|
283
|
+
}
|
|
278
284
|
get killed() {
|
|
279
285
|
return this.process.killed || !this.process.connected;
|
|
280
286
|
}
|
|
@@ -343,7 +349,7 @@ var ClusterProcess = class _ClusterProcess extends EventEmitter {
|
|
|
343
349
|
#handleIPC(message) {
|
|
344
350
|
if (this.#isValidPayload(message)) {
|
|
345
351
|
if (isDispatchPayload(message)) {
|
|
346
|
-
this
|
|
352
|
+
this.#handleDispatchPayload(message);
|
|
347
353
|
return;
|
|
348
354
|
}
|
|
349
355
|
if (isEvalResponsePayload(message)) {
|
|
@@ -352,6 +358,9 @@ var ClusterProcess = class _ClusterProcess extends EventEmitter {
|
|
|
352
358
|
}
|
|
353
359
|
}
|
|
354
360
|
}
|
|
361
|
+
#handleDispatchPayload(payload) {
|
|
362
|
+
payload.t === "shardAdd" && this.#shards.add(payload.d[0]), this.emit(payload.t, ...payload.d);
|
|
363
|
+
}
|
|
355
364
|
#handleEvalResponse(payload) {
|
|
356
365
|
let pending = this.#pendingEvals.get(payload.d.nonce);
|
|
357
366
|
if (!pending) {
|
|
@@ -529,22 +538,21 @@ var ShardingManager = class extends EventEmitter {
|
|
|
529
538
|
return this.#totalShards;
|
|
530
539
|
}
|
|
531
540
|
get ready() {
|
|
532
|
-
return this.#readyCount === this.totalClusters;
|
|
541
|
+
return this.totalClusters > 0 && this.#readyCount === this.totalClusters;
|
|
533
542
|
}
|
|
534
543
|
async spawn() {
|
|
535
544
|
this.#gatewayInfo = await this.rest.get("/gateway/bot");
|
|
536
545
|
let { session_start_limit: limit } = this.#gatewayInfo;
|
|
537
|
-
this.#totalShards = typeof this.options.totalShards == "number" ? this.options.totalShards : this.#gatewayInfo.shards, this.#identifyQueue = new
|
|
546
|
+
this.#totalShards = typeof this.options.totalShards == "number" ? this.options.totalShards : this.#gatewayInfo.shards, this.#identifyQueue = new PQueue({
|
|
538
547
|
concurrency: limit.max_concurrency,
|
|
539
548
|
intervalCap: limit.max_concurrency,
|
|
540
549
|
interval: 5e3
|
|
541
550
|
});
|
|
542
551
|
let { totalShards, totalClusters } = this;
|
|
543
552
|
this.emit("debug", `Spawning ${totalClusters} clusters (${totalShards} total shards)...`);
|
|
544
|
-
let promises = [];
|
|
545
553
|
for (let i = 0; i < totalClusters; i++)
|
|
546
|
-
|
|
547
|
-
|
|
554
|
+
this.#spawnCluster(i);
|
|
555
|
+
this.emit("debug", "All clusters spawned");
|
|
548
556
|
}
|
|
549
557
|
async kill(signal = "SIGTERM") {
|
|
550
558
|
this.emit("debug", "Shutting down all clusters...");
|
|
@@ -565,6 +573,12 @@ var ShardingManager = class extends EventEmitter {
|
|
|
565
573
|
let promises = this.clusters.map((cluster) => cluster.eval(fn));
|
|
566
574
|
return Promise.all(promises);
|
|
567
575
|
}
|
|
576
|
+
send(shardId, payload) {
|
|
577
|
+
let cluster = this.clusters.find((cluster2) => cluster2.shards.has(shardId));
|
|
578
|
+
if (!cluster)
|
|
579
|
+
throw new Error(`Shard ${shardId} not found`);
|
|
580
|
+
cluster.send(payload);
|
|
581
|
+
}
|
|
568
582
|
requestIdentify(cluster, shardId) {
|
|
569
583
|
this.#identifyQueue?.add(() => cluster.identifyShard(shardId));
|
|
570
584
|
}
|
|
@@ -572,7 +586,7 @@ var ShardingManager = class extends EventEmitter {
|
|
|
572
586
|
let start = clusterId * this.options.shardsPerCluster, end = Math.min(start + this.options.shardsPerCluster, this.totalShards);
|
|
573
587
|
return Array.from({ length: end - start }, (_, i) => start + i);
|
|
574
588
|
}
|
|
575
|
-
|
|
589
|
+
#spawnCluster(id) {
|
|
576
590
|
let shardIds = this.#getShardIdsForCluster(id), firstShardId = shardIds[0], lastShardId = shardIds[shardIds.length - 1];
|
|
577
591
|
this.emit("debug", `Spawning cluster ${id} (shards ${firstShardId}-${lastShardId})`);
|
|
578
592
|
let env = {
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakit/gateway",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Gateway manager for bakit framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"import":
|
|
9
|
-
|
|
8
|
+
"import": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"require": {
|
|
13
|
+
"types": "./dist/index.d.cts",
|
|
14
|
+
"default": "./dist/index.cjs"
|
|
15
|
+
}
|
|
10
16
|
}
|
|
11
17
|
},
|
|
12
18
|
"files": [
|
|
@@ -28,11 +34,13 @@
|
|
|
28
34
|
"author": "louiszn",
|
|
29
35
|
"license": "MIT",
|
|
30
36
|
"dependencies": {
|
|
31
|
-
"
|
|
37
|
+
"@discordjs/collection": "^2.1.1",
|
|
38
|
+
"discord-api-types": "^0.38.38",
|
|
39
|
+
"p-queue": "^9.1.0",
|
|
32
40
|
"type-fest": "^4.41.0",
|
|
33
41
|
"ws": "^8.19.0",
|
|
34
|
-
"@bakit/
|
|
35
|
-
"@bakit/
|
|
42
|
+
"@bakit/rest": "^3.0.2",
|
|
43
|
+
"@bakit/utils": "^3.0.0"
|
|
36
44
|
},
|
|
37
45
|
"devDependencies": {
|
|
38
46
|
"@types/ws": "^8.18.1"
|