lunibee 0.3.3 → 0.4.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/README.md +1 -1
- package/dist/builders/commands.d.ts +1 -1
- package/dist/builders/components.d.ts +9 -11
- package/dist/builders/embed.d.ts +1 -1
- package/dist/builders/index.d.ts +1 -1
- package/dist/builders/index.js +87 -1298
- package/dist/builders/index.js.map +3 -11
- package/dist/chunks/index-0wn81eng.js +2492 -0
- package/dist/chunks/index-0wn81eng.js.map +17 -0
- package/dist/chunks/index-2en489j6.js +40 -0
- package/dist/chunks/index-2en489j6.js.map +10 -0
- package/dist/chunks/index-3hyps48a.js +203 -0
- package/dist/chunks/index-3hyps48a.js.map +11 -0
- package/dist/chunks/index-57c5n3ms.js +2274 -0
- package/dist/chunks/index-57c5n3ms.js.map +19 -0
- package/dist/chunks/index-5yx92e36.js +1170 -0
- package/dist/chunks/index-5yx92e36.js.map +14 -0
- package/dist/chunks/index-690bp093.js +95 -0
- package/dist/chunks/index-690bp093.js.map +10 -0
- package/dist/chunks/index-7fp2hvmg.js +2155 -0
- package/dist/chunks/index-7fp2hvmg.js.map +24 -0
- package/dist/chunks/index-cnmsajvk.js +1119 -0
- package/dist/chunks/index-cnmsajvk.js.map +12 -0
- package/dist/chunks/index-fdvgk2sw.js +1310 -0
- package/dist/chunks/index-fdvgk2sw.js.map +21 -0
- package/dist/chunks/index-gyvps8b9.js +214 -0
- package/dist/chunks/index-gyvps8b9.js.map +12 -0
- package/dist/chunks/index-jzg4bh99.js +1533 -0
- package/dist/chunks/index-jzg4bh99.js.map +21 -0
- package/dist/chunks/index-mgw2yvrm.js +3547 -0
- package/dist/chunks/index-mgw2yvrm.js.map +25 -0
- package/dist/chunks/index-yh76b55h.js +3449 -0
- package/dist/chunks/index-yh76b55h.js.map +29 -0
- package/dist/collection/index.js +9 -1118
- package/dist/collection/index.js.map +3 -6
- package/dist/core/api.d.ts +3 -3
- package/dist/core/channel-create.d.ts +2 -2
- package/dist/core/commands.d.ts +3 -3
- package/dist/core/events.d.ts +4 -4
- package/dist/core/handles.d.ts +4 -4
- package/dist/core/index.d.ts +25 -20
- package/dist/core/index.js +68 -10130
- package/dist/core/index.js.map +3 -65
- package/dist/core/permissions.d.ts +1 -1
- package/dist/core/redis-cache.d.ts +1 -1
- package/dist/formatters/index.js +41 -110
- package/dist/formatters/index.js.map +3 -4
- package/dist/handlers/index.js +5 -37
- package/dist/handlers/index.js.map +3 -4
- package/dist/index.d.ts +21 -21
- package/dist/index.js +641 -16325
- package/dist/index.js.map +3 -92
- package/dist/managers/advanced.d.ts +3 -3
- package/dist/managers/application.d.ts +2 -2
- package/dist/managers/base.d.ts +1 -1
- package/dist/managers/emoji.d.ts +2 -2
- package/dist/managers/guild-resources.d.ts +6 -6
- package/dist/managers/guild.d.ts +3 -3
- package/dist/managers/index.d.ts +4 -8
- package/dist/managers/index.js +51 -4060
- package/dist/managers/index.js.map +3 -29
- package/dist/managers/member.d.ts +2 -2
- package/dist/managers/message.d.ts +14 -6
- package/dist/managers/role.d.ts +2 -2
- package/dist/managers/thread.d.ts +3 -3
- package/dist/managers/user.d.ts +7 -4
- package/dist/rest/index.d.ts +10 -35
- package/dist/rest/index.js +57 -2184
- package/dist/rest/index.js.map +3 -18
- package/dist/rest/limiter.d.ts +9 -1
- package/dist/rest/webhook.d.ts +2 -8
- package/dist/sharding/bus.d.ts +2 -2
- package/dist/sharding/cluster.d.ts +45 -5
- package/dist/sharding/context.d.ts +45 -3
- package/dist/sharding/gateway-bot.d.ts +2 -1
- package/dist/sharding/health.d.ts +1 -1
- package/dist/sharding/identify.d.ts +12 -0
- package/dist/sharding/index.d.ts +44 -11
- package/dist/sharding/index.js +60 -4620
- package/dist/sharding/index.js.map +3 -33
- package/dist/sharding/supervisor.d.ts +4 -2
- package/dist/structures/audit-log.d.ts +1 -1
- package/dist/structures/base.d.ts +6 -6
- package/dist/structures/channels.d.ts +1 -1
- package/dist/structures/embed.d.ts +1 -1
- package/dist/structures/index.d.ts +14 -14
- package/dist/structures/index.js +109 -2382
- package/dist/structures/index.js.map +3 -16
- package/dist/structures/interactions.d.ts +13 -9
- package/dist/structures/options.d.ts +1 -1
- package/dist/structures/permissions.d.ts +2 -2
- package/dist/structures/resources.d.ts +10 -10
- package/dist/structures/sticker.d.ts +1 -1
- package/dist/types/gateway.d.ts +2 -79
- package/dist/types/index.d.ts +0 -6
- package/dist/types/index.js +41 -272
- package/dist/types/index.js.map +3 -6
- package/dist/utils/index.js +23 -209
- package/dist/utils/index.js.map +3 -5
- package/dist/voice/connection.d.ts +200 -0
- package/dist/voice/crypto.d.ts +50 -0
- package/dist/voice/dave.d.ts +85 -0
- package/dist/voice/effects.d.ts +31 -0
- package/dist/voice/emitter.d.ts +14 -0
- package/dist/voice/gateway.d.ts +59 -0
- package/dist/voice/index.d.ts +25 -228
- package/dist/voice/index.js +111 -442
- package/dist/voice/index.js.map +3 -4
- package/dist/voice/libopus.d.ts +88 -0
- package/dist/voice/networking.d.ts +80 -0
- package/dist/voice/ogg.d.ts +24 -0
- package/dist/voice/opus.d.ts +18 -0
- package/dist/voice/player.d.ts +79 -0
- package/dist/voice/protocol.d.ts +76 -0
- package/dist/voice/receiver.d.ts +59 -0
- package/dist/voice/resource.d.ts +102 -0
- package/dist/voice/udp.d.ts +40 -0
- package/dist/ws/close-codes.d.ts +1 -1
- package/dist/ws/index.d.ts +25 -4
- package/dist/ws/index.js +47 -1336
- package/dist/ws/index.js.map +3 -17
- package/dist/ws/protocol.d.ts +1 -1
- package/dist/ws/send-budget.d.ts +0 -2
- package/dist/ws/state.d.ts +0 -10
- package/package.json +19 -16
package/dist/sharding/index.js
CHANGED
|
@@ -1,4625 +1,65 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
name: "@lunibee/sharding",
|
|
5
|
-
version: "0.3.3",
|
|
6
|
-
type: "module",
|
|
7
|
-
module: "./src/index.ts",
|
|
8
|
-
types: "./src/index.ts",
|
|
9
|
-
exports: {
|
|
10
|
-
".": "./src/index.ts"
|
|
11
|
-
},
|
|
12
|
-
dependencies: {
|
|
13
|
-
"@lunibee/ws": "workspace:*"
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// packages/sharding/src/gateway-bot.ts
|
|
18
|
-
async function fetchGatewayBot(token) {
|
|
19
|
-
const response = await fetch("https://discord.com/api/v10/gateway/bot", {
|
|
20
|
-
headers: {
|
|
21
|
-
Authorization: `Bot ${token}`,
|
|
22
|
-
"User-Agent": `Lunibee/${package_default.version}`
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
if (!response.ok)
|
|
26
|
-
throw new Error(`Gateway discovery failed with status ${response.status}`);
|
|
27
|
-
const data = await response.json();
|
|
28
|
-
if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1)
|
|
29
|
-
throw new Error("Gateway discovery returned an invalid shard count.");
|
|
30
|
-
const limit = data.session_start_limit;
|
|
31
|
-
const info = { shards: data.shards };
|
|
32
|
-
if (limit && typeof limit.total === "number" && typeof limit.remaining === "number" && typeof limit.reset_after === "number" && typeof limit.max_concurrency === "number")
|
|
33
|
-
info.sessionStartLimit = {
|
|
34
|
-
total: limit.total,
|
|
35
|
-
remaining: limit.remaining,
|
|
36
|
-
resetAfter: limit.reset_after,
|
|
37
|
-
maxConcurrency: Math.max(1, limit.max_concurrency)
|
|
38
|
-
};
|
|
39
|
-
return info;
|
|
40
|
-
}
|
|
41
|
-
var MAX_GUILDS_PER_SHARD = 2500;
|
|
42
|
-
function assertGuildsPerShard(value) {
|
|
43
|
-
if (!Number.isInteger(value) || value < 1 || value > MAX_GUILDS_PER_SHARD)
|
|
44
|
-
throw new RangeError(`guildsPerShard must be an integer from 1 to ${MAX_GUILDS_PER_SHARD}.`);
|
|
45
|
-
}
|
|
46
|
-
function shardsForGuilds(guilds, guildsPerShard) {
|
|
47
|
-
assertGuildsPerShard(guildsPerShard);
|
|
48
|
-
if (!Number.isInteger(guilds) || guilds < 0)
|
|
49
|
-
throw new RangeError("guildCount must be a non-negative integer.");
|
|
50
|
-
return Math.max(1, Math.ceil(guilds / guildsPerShard));
|
|
51
|
-
}
|
|
52
|
-
async function fetchGuildCount(token) {
|
|
53
|
-
const response = await fetch("https://discord.com/api/v10/applications/@me", {
|
|
54
|
-
headers: {
|
|
55
|
-
Authorization: `Bot ${token}`,
|
|
56
|
-
"User-Agent": `Lunibee/${package_default.version}`
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
if (!response.ok)
|
|
60
|
-
throw new Error(`Guild count discovery failed with status ${response.status}`);
|
|
61
|
-
const data = await response.json();
|
|
62
|
-
const count = data.approximate_guild_count;
|
|
63
|
-
if (typeof count !== "number" || !Number.isInteger(count) || count < 0)
|
|
64
|
-
throw new Error("Discord did not report a guild count.");
|
|
65
|
-
return count;
|
|
66
|
-
}
|
|
67
|
-
async function resolveShardCount(token, info, options) {
|
|
68
|
-
if (options.guildsPerShard === undefined)
|
|
69
|
-
return info.shards;
|
|
70
|
-
return shardsForGuilds(options.guildCount ?? await fetchGuildCount(token), options.guildsPerShard);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// packages/ws/src/session.ts
|
|
74
|
-
class GatewaySession {
|
|
75
|
-
#state = { status: "none" };
|
|
76
|
-
#sequence = null;
|
|
77
|
-
#generation = 0;
|
|
78
|
-
#lastInvalidation;
|
|
79
|
-
get lastInvalidation() {
|
|
80
|
-
return this.#lastInvalidation;
|
|
81
|
-
}
|
|
82
|
-
get generation() {
|
|
83
|
-
return this.#generation;
|
|
84
|
-
}
|
|
85
|
-
get sessionId() {
|
|
86
|
-
return this.#state.status === "established" ? this.#state.sessionId : undefined;
|
|
87
|
-
}
|
|
88
|
-
get resumeURL() {
|
|
89
|
-
return this.#state.status === "established" ? this.#state.resumeURL : undefined;
|
|
90
|
-
}
|
|
91
|
-
get sequence() {
|
|
92
|
-
return this.#sequence;
|
|
93
|
-
}
|
|
94
|
-
get canResume() {
|
|
95
|
-
return this.#state.status === "established" && this.#sequence !== null;
|
|
96
|
-
}
|
|
97
|
-
beginConnection() {
|
|
98
|
-
return ++this.#generation;
|
|
99
|
-
}
|
|
100
|
-
owns(token) {
|
|
101
|
-
return token === this.#generation;
|
|
102
|
-
}
|
|
103
|
-
recordSequence(sequence, token) {
|
|
104
|
-
if (!this.owns(token))
|
|
105
|
-
return false;
|
|
106
|
-
if (!Number.isInteger(sequence))
|
|
107
|
-
return false;
|
|
108
|
-
if (this.#sequence !== null && sequence < this.#sequence)
|
|
109
|
-
return false;
|
|
110
|
-
this.#sequence = sequence;
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
activate(payload, token) {
|
|
114
|
-
if (!this.owns(token))
|
|
115
|
-
return false;
|
|
116
|
-
const ready = payload;
|
|
117
|
-
if (typeof ready?.session_id !== "string" || typeof ready?.resume_gateway_url !== "string")
|
|
118
|
-
return false;
|
|
119
|
-
this.#state = {
|
|
120
|
-
status: "established",
|
|
121
|
-
sessionId: ready.session_id,
|
|
122
|
-
resumeURL: ready.resume_gateway_url
|
|
123
|
-
};
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
invalidate(token, reason = "explicit") {
|
|
127
|
-
if (!this.owns(token))
|
|
128
|
-
return false;
|
|
129
|
-
this.#lastInvalidation = reason;
|
|
130
|
-
this.#state = { status: "none" };
|
|
131
|
-
this.#sequence = null;
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
resumeInfo() {
|
|
135
|
-
if (this.#state.status !== "established")
|
|
136
|
-
return;
|
|
137
|
-
if (this.#sequence === null)
|
|
138
|
-
return;
|
|
139
|
-
const { sessionId, resumeURL } = this.#state;
|
|
140
|
-
return { sessionId, sequence: this.#sequence, resumeURL };
|
|
141
|
-
}
|
|
142
|
-
handshake() {
|
|
143
|
-
const resume = this.resumeInfo();
|
|
144
|
-
return resume ? { type: "resume", ...resume } : { type: "identify" };
|
|
145
|
-
}
|
|
146
|
-
connectURL(fallback) {
|
|
147
|
-
const resume = this.resumeInfo()?.resumeURL;
|
|
148
|
-
return resume !== undefined && isTrustedGatewayURL(resume, fallback) ? resume : fallback;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
function isTrustedGatewayURL(url, fallback) {
|
|
152
|
-
let parsed;
|
|
153
|
-
try {
|
|
154
|
-
parsed = new URL(url);
|
|
155
|
-
} catch {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
if (parsed.protocol !== "wss:" || parsed.username || parsed.password)
|
|
159
|
-
return false;
|
|
160
|
-
const host = parsed.hostname.toLowerCase();
|
|
161
|
-
if (host === "discord.gg" || host.endsWith(".discord.gg"))
|
|
162
|
-
return true;
|
|
163
|
-
try {
|
|
164
|
-
return host === new URL(fallback).hostname.toLowerCase();
|
|
165
|
-
} catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// packages/ws/src/heartbeat.ts
|
|
171
|
-
var MAX_MONITOR_INTERVAL = 1000;
|
|
172
|
-
|
|
173
|
-
class GatewayHeartbeat {
|
|
174
|
-
#options;
|
|
175
|
-
#now;
|
|
176
|
-
#interval = 0;
|
|
177
|
-
#acknowledged = true;
|
|
178
|
-
#sentAt = 0;
|
|
179
|
-
#latency = -1;
|
|
180
|
-
#lastMessageAt = 0;
|
|
181
|
-
#zombieReported = false;
|
|
182
|
-
#timer;
|
|
183
|
-
#initialTimer;
|
|
184
|
-
#ackTimer;
|
|
185
|
-
#monitorTimer;
|
|
186
|
-
constructor(options) {
|
|
187
|
-
if (options.zombieTimeout <= options.ackTimeout)
|
|
188
|
-
throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
|
|
189
|
-
this.#options = options;
|
|
190
|
-
this.#now = options.now ?? (() => Date.now());
|
|
191
|
-
}
|
|
192
|
-
get latency() {
|
|
193
|
-
return this.#latency;
|
|
194
|
-
}
|
|
195
|
-
get interval() {
|
|
196
|
-
return this.#interval;
|
|
197
|
-
}
|
|
198
|
-
get acknowledged() {
|
|
199
|
-
return this.#acknowledged;
|
|
200
|
-
}
|
|
201
|
-
get silentFor() {
|
|
202
|
-
return this.#lastMessageAt === 0 ? 0 : this.#now() - this.#lastMessageAt;
|
|
203
|
-
}
|
|
204
|
-
get stalenessDeadline() {
|
|
205
|
-
return Math.max(this.#options.zombieTimeout, this.#interval + this.#options.ackTimeout);
|
|
206
|
-
}
|
|
207
|
-
isHealthy() {
|
|
208
|
-
if (!this.#acknowledged)
|
|
209
|
-
return false;
|
|
210
|
-
if (this.#lastMessageAt === 0)
|
|
211
|
-
return true;
|
|
212
|
-
return this.silentFor < this.stalenessDeadline;
|
|
213
|
-
}
|
|
214
|
-
watch() {
|
|
215
|
-
this.#lastMessageAt = this.#now();
|
|
216
|
-
this.#zombieReported = false;
|
|
217
|
-
this.#startMonitor();
|
|
218
|
-
}
|
|
219
|
-
receivedMessage() {
|
|
220
|
-
this.#lastMessageAt = this.#now();
|
|
221
|
-
this.#zombieReported = false;
|
|
222
|
-
}
|
|
223
|
-
start(interval) {
|
|
224
|
-
this.#clearBeatTimers();
|
|
225
|
-
this.#interval = interval;
|
|
226
|
-
this.#acknowledged = true;
|
|
227
|
-
this.#initialTimer = setTimeout(() => {
|
|
228
|
-
this.#initialTimer = undefined;
|
|
229
|
-
this.sendHeartbeat();
|
|
230
|
-
this.#timer = setInterval(() => this.sendHeartbeat(), interval);
|
|
231
|
-
}, Math.random() * interval);
|
|
232
|
-
}
|
|
233
|
-
stop() {
|
|
234
|
-
this.#clearBeatTimers();
|
|
235
|
-
if (this.#monitorTimer)
|
|
236
|
-
clearInterval(this.#monitorTimer);
|
|
237
|
-
this.#monitorTimer = undefined;
|
|
238
|
-
}
|
|
239
|
-
sendHeartbeat() {
|
|
240
|
-
this.#acknowledged = false;
|
|
241
|
-
this.#sentAt = this.#now();
|
|
242
|
-
if (!this.#options.send(this.#options.sequence())) {
|
|
243
|
-
this.#options.onError(new Error("Unable to send Gateway heartbeat because the WebSocket is not open."));
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
this.#clearAckTimer();
|
|
247
|
-
this.#ackTimer = setTimeout(() => {
|
|
248
|
-
if (this.#acknowledged)
|
|
249
|
-
return;
|
|
250
|
-
this.#options.onTimeout({
|
|
251
|
-
type: "ack",
|
|
252
|
-
elapsedMs: this.#now() - this.#sentAt
|
|
253
|
-
});
|
|
254
|
-
}, this.#options.ackTimeout);
|
|
255
|
-
}
|
|
256
|
-
acknowledge() {
|
|
257
|
-
if (this.#acknowledged)
|
|
258
|
-
return;
|
|
259
|
-
this.#acknowledged = true;
|
|
260
|
-
this.#latency = this.#now() - this.#sentAt;
|
|
261
|
-
this.#clearAckTimer();
|
|
262
|
-
}
|
|
263
|
-
#startMonitor() {
|
|
264
|
-
if (this.#monitorTimer)
|
|
265
|
-
clearInterval(this.#monitorTimer);
|
|
266
|
-
const interval = Math.max(1, Math.min(MAX_MONITOR_INTERVAL, this.#options.ackTimeout, this.#options.zombieTimeout / 2));
|
|
267
|
-
this.#monitorTimer = setInterval(() => this.#checkStaleness(), interval);
|
|
268
|
-
}
|
|
269
|
-
#checkStaleness() {
|
|
270
|
-
if (!this.#options.isConnected() || this.#lastMessageAt === 0)
|
|
271
|
-
return;
|
|
272
|
-
const silentFor = this.silentFor;
|
|
273
|
-
const deadline = this.stalenessDeadline;
|
|
274
|
-
if (silentFor < deadline || this.#zombieReported)
|
|
275
|
-
return;
|
|
276
|
-
this.#zombieReported = true;
|
|
277
|
-
this.#options.onTimeout({ type: "zombie", silentFor, deadline });
|
|
278
|
-
}
|
|
279
|
-
#clearAckTimer() {
|
|
280
|
-
if (this.#ackTimer)
|
|
281
|
-
clearTimeout(this.#ackTimer);
|
|
282
|
-
this.#ackTimer = undefined;
|
|
283
|
-
}
|
|
284
|
-
#clearBeatTimers() {
|
|
285
|
-
if (this.#timer)
|
|
286
|
-
clearInterval(this.#timer);
|
|
287
|
-
if (this.#initialTimer)
|
|
288
|
-
clearTimeout(this.#initialTimer);
|
|
289
|
-
this.#clearAckTimer();
|
|
290
|
-
this.#timer = undefined;
|
|
291
|
-
this.#initialTimer = undefined;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// packages/ws/src/close-codes.ts
|
|
296
|
-
var GatewayCloseCodes = {
|
|
297
|
-
UnknownError: 4000,
|
|
298
|
-
UnknownOpcode: 4001,
|
|
299
|
-
DecodeError: 4002,
|
|
300
|
-
NotAuthenticated: 4003,
|
|
301
|
-
AuthenticationFailed: 4004,
|
|
302
|
-
AlreadyAuthenticated: 4005,
|
|
303
|
-
InvalidSeq: 4007,
|
|
304
|
-
RateLimited: 4008,
|
|
305
|
-
SessionTimedOut: 4009,
|
|
306
|
-
InvalidShard: 4010,
|
|
307
|
-
ShardingRequired: 4011,
|
|
308
|
-
InvalidAPIVersion: 4012,
|
|
309
|
-
InvalidIntents: 4013,
|
|
310
|
-
DisallowedIntents: 4014
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// packages/ws/src/reconnect.ts
|
|
314
|
-
var FATAL_CLOSE_CODES = [
|
|
315
|
-
GatewayCloseCodes.AuthenticationFailed,
|
|
316
|
-
GatewayCloseCodes.InvalidShard,
|
|
317
|
-
GatewayCloseCodes.ShardingRequired,
|
|
318
|
-
GatewayCloseCodes.InvalidAPIVersion,
|
|
319
|
-
GatewayCloseCodes.InvalidIntents,
|
|
320
|
-
GatewayCloseCodes.DisallowedIntents
|
|
321
|
-
];
|
|
322
|
-
var IDENTIFY_CLOSE_CODES = [
|
|
323
|
-
GatewayCloseCodes.InvalidSeq,
|
|
324
|
-
GatewayCloseCodes.SessionTimedOut
|
|
325
|
-
];
|
|
326
|
-
function classifyCloseCode(code, canResume) {
|
|
327
|
-
if (FATAL_CLOSE_CODES.includes(code))
|
|
328
|
-
return "stop";
|
|
329
|
-
if (IDENTIFY_CLOSE_CODES.includes(code))
|
|
330
|
-
return "identify";
|
|
331
|
-
return canResume ? "resume" : "identify";
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
class GatewayReconnect {
|
|
335
|
-
#options;
|
|
336
|
-
#random;
|
|
337
|
-
#attempt = 0;
|
|
338
|
-
#recent = [];
|
|
339
|
-
#timer;
|
|
340
|
-
#promise;
|
|
341
|
-
#resolve;
|
|
342
|
-
#reject;
|
|
343
|
-
constructor(options) {
|
|
344
|
-
this.#options = options;
|
|
345
|
-
this.#random = options.random ?? Math.random;
|
|
346
|
-
}
|
|
347
|
-
get enabled() {
|
|
348
|
-
return this.#options.enabled;
|
|
349
|
-
}
|
|
350
|
-
get attempts() {
|
|
351
|
-
return this.#attempt;
|
|
352
|
-
}
|
|
353
|
-
get pending() {
|
|
354
|
-
return this.#timer !== undefined;
|
|
355
|
-
}
|
|
356
|
-
get exhausted() {
|
|
357
|
-
return this.#attempt >= this.#options.maxAttempts;
|
|
358
|
-
}
|
|
359
|
-
get connecting() {
|
|
360
|
-
return this.#promise !== undefined;
|
|
361
|
-
}
|
|
362
|
-
get attemptPromise() {
|
|
363
|
-
return this.#promise;
|
|
364
|
-
}
|
|
365
|
-
classifyClose(code, canResume) {
|
|
366
|
-
return classifyCloseCode(code, canResume);
|
|
367
|
-
}
|
|
368
|
-
canReconnect() {
|
|
369
|
-
return this.#options.enabled && !this.exhausted;
|
|
370
|
-
}
|
|
371
|
-
schedule(run) {
|
|
372
|
-
if (!this.#options.enabled)
|
|
373
|
-
return { scheduled: false, reason: "disabled" };
|
|
374
|
-
if (this.#timer)
|
|
375
|
-
return { scheduled: false, reason: "pending" };
|
|
376
|
-
if (this.exhausted)
|
|
377
|
-
return { scheduled: false, reason: "exhausted" };
|
|
378
|
-
const attempt = this.#attempt;
|
|
379
|
-
const now = (this.#options.now ?? Date.now)();
|
|
380
|
-
const window = this.#options.stormWindow ?? 60000;
|
|
381
|
-
const limit = this.#options.stormLimit ?? 5;
|
|
382
|
-
while (this.#recent.length && now - this.#recent[0] >= window)
|
|
383
|
-
this.#recent.shift();
|
|
384
|
-
const over = this.#recent.length - limit;
|
|
385
|
-
this.#recent.push(now);
|
|
386
|
-
let delay = Math.min(this.#options.maxDelay, this.#options.baseDelay * 2 ** this.#attempt++);
|
|
387
|
-
const unstable = over >= 0;
|
|
388
|
-
if (unstable)
|
|
389
|
-
delay = Math.min(this.#options.maxDelay, Math.max(delay, this.#options.baseDelay * 2 ** (over + 1)));
|
|
390
|
-
const jitter = this.#random() * Math.max(1, delay * 0.25);
|
|
391
|
-
const delayMs = delay + jitter;
|
|
392
|
-
this.#timer = setTimeout(() => {
|
|
393
|
-
this.#timer = undefined;
|
|
394
|
-
run();
|
|
395
|
-
}, delayMs);
|
|
396
|
-
return {
|
|
397
|
-
scheduled: true,
|
|
398
|
-
delayMs,
|
|
399
|
-
attempt,
|
|
400
|
-
...unstable ? { unstable: { reconnects: this.#recent.length } } : {}
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
cancel() {
|
|
404
|
-
if (this.#timer)
|
|
405
|
-
clearTimeout(this.#timer);
|
|
406
|
-
this.#timer = undefined;
|
|
407
|
-
}
|
|
408
|
-
reset() {
|
|
409
|
-
this.#attempt = 0;
|
|
410
|
-
}
|
|
411
|
-
attempt(start) {
|
|
412
|
-
if (this.#promise)
|
|
413
|
-
return this.#promise;
|
|
414
|
-
this.#promise = new Promise((resolve, reject) => {
|
|
415
|
-
this.#resolve = resolve;
|
|
416
|
-
this.#reject = reject;
|
|
417
|
-
start();
|
|
418
|
-
});
|
|
419
|
-
return this.#promise;
|
|
420
|
-
}
|
|
421
|
-
settle(error) {
|
|
422
|
-
const resolve = this.#resolve;
|
|
423
|
-
const reject = this.#reject;
|
|
424
|
-
this.#resolve = undefined;
|
|
425
|
-
this.#reject = undefined;
|
|
426
|
-
this.#promise = undefined;
|
|
427
|
-
if (error)
|
|
428
|
-
reject?.(error);
|
|
429
|
-
else
|
|
430
|
-
resolve?.();
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// packages/ws/src/decoder.ts
|
|
435
|
-
import {
|
|
436
|
-
createInflate,
|
|
437
|
-
constants as zlibConstants
|
|
438
|
-
} from "zlib";
|
|
439
|
-
async function toBytes(data) {
|
|
440
|
-
if (data instanceof Uint8Array)
|
|
441
|
-
return data;
|
|
442
|
-
if (data instanceof ArrayBuffer)
|
|
443
|
-
return new Uint8Array(data);
|
|
444
|
-
if (typeof Blob !== "undefined" && data instanceof Blob)
|
|
445
|
-
return new Uint8Array(await data.arrayBuffer());
|
|
446
|
-
throw new TypeError("Gateway returned an unsupported compressed frame type.");
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
class PlainTextDecoder {
|
|
450
|
-
push(data) {
|
|
451
|
-
return [String(data)];
|
|
452
|
-
}
|
|
453
|
-
reset() {}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
class ZlibStreamDecoder {
|
|
457
|
-
#create;
|
|
458
|
-
#maxFrameBytes;
|
|
459
|
-
#inflate;
|
|
460
|
-
#chunks = [];
|
|
461
|
-
#size = 0;
|
|
462
|
-
#failure;
|
|
463
|
-
#pendingReject;
|
|
464
|
-
constructor(create = () => createInflate(), maxFrameBytes = 64 * 1024 * 1024) {
|
|
465
|
-
this.#create = create;
|
|
466
|
-
this.#maxFrameBytes = maxFrameBytes;
|
|
467
|
-
}
|
|
468
|
-
push(data) {
|
|
469
|
-
if (typeof data === "string")
|
|
470
|
-
return [data];
|
|
471
|
-
return this.#inflateFrame(data);
|
|
472
|
-
}
|
|
473
|
-
async#inflateFrame(data) {
|
|
474
|
-
const chunk = await toBytes(data);
|
|
475
|
-
const inflate = this.#inflate ??= this.#open();
|
|
476
|
-
if (this.#failure)
|
|
477
|
-
throw this.#failure;
|
|
478
|
-
const complete = chunk.length >= 4 && chunk[chunk.length - 4] === 0 && chunk[chunk.length - 3] === 0 && chunk[chunk.length - 2] === 255 && chunk[chunk.length - 1] === 255;
|
|
479
|
-
await this.#awaiting((settle) => inflate.write(chunk, (error) => error ? settle(error) : settle()));
|
|
480
|
-
if (!complete)
|
|
481
|
-
return [];
|
|
482
|
-
await this.#awaiting((settle) => inflate.flush(zlibConstants.Z_SYNC_FLUSH, () => settle()));
|
|
483
|
-
const parts = this.#chunks;
|
|
484
|
-
this.#chunks = [];
|
|
485
|
-
this.#size = 0;
|
|
486
|
-
if (parts.length === 0)
|
|
487
|
-
return [];
|
|
488
|
-
const total = parts.reduce((size, part) => size + part.length, 0);
|
|
489
|
-
const merged = new Uint8Array(total);
|
|
490
|
-
let offset = 0;
|
|
491
|
-
for (const part of parts) {
|
|
492
|
-
merged.set(part, offset);
|
|
493
|
-
offset += part.length;
|
|
494
|
-
}
|
|
495
|
-
return [new TextDecoder().decode(merged)];
|
|
496
|
-
}
|
|
497
|
-
reset() {
|
|
498
|
-
const inflate = this.#inflate;
|
|
499
|
-
this.#inflate = undefined;
|
|
500
|
-
this.#chunks = [];
|
|
501
|
-
this.#size = 0;
|
|
502
|
-
this.#failure = undefined;
|
|
503
|
-
this.#pendingReject = undefined;
|
|
504
|
-
if (!inflate)
|
|
505
|
-
return;
|
|
506
|
-
inflate.removeAllListeners();
|
|
507
|
-
try {
|
|
508
|
-
inflate.close();
|
|
509
|
-
} catch {}
|
|
510
|
-
}
|
|
511
|
-
#awaiting(run) {
|
|
512
|
-
if (this.#failure)
|
|
513
|
-
return Promise.reject(this.#failure);
|
|
514
|
-
return new Promise((resolve, reject) => {
|
|
515
|
-
this.#pendingReject = reject;
|
|
516
|
-
run((error) => {
|
|
517
|
-
this.#pendingReject = undefined;
|
|
518
|
-
if (error)
|
|
519
|
-
reject(error);
|
|
520
|
-
else if (this.#failure)
|
|
521
|
-
reject(this.#failure);
|
|
522
|
-
else
|
|
523
|
-
resolve();
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
#open() {
|
|
528
|
-
const inflate = this.#create();
|
|
529
|
-
inflate.on("data", (chunk) => {
|
|
530
|
-
if (this.#failure)
|
|
531
|
-
return;
|
|
532
|
-
this.#size += chunk.length;
|
|
533
|
-
if (this.#size > this.#maxFrameBytes) {
|
|
534
|
-
this.#chunks = [];
|
|
535
|
-
inflate.emit("error", new Error(`Gateway frame inflated past ${this.#maxFrameBytes} bytes.`));
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
this.#chunks.push(chunk);
|
|
539
|
-
});
|
|
540
|
-
inflate.on("error", (error) => {
|
|
541
|
-
this.#failure = error;
|
|
542
|
-
const reject = this.#pendingReject;
|
|
543
|
-
this.#pendingReject = undefined;
|
|
544
|
-
reject?.(error);
|
|
545
|
-
});
|
|
546
|
-
return inflate;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
function createDecoder(compress) {
|
|
550
|
-
return compress ? new ZlibStreamDecoder : new PlainTextDecoder;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// packages/ws/src/transport.ts
|
|
554
|
-
var OPEN = 1;
|
|
555
|
-
|
|
556
|
-
class WebSocketTransport {
|
|
557
|
-
#handlers;
|
|
558
|
-
#decoder;
|
|
559
|
-
#createSocket;
|
|
560
|
-
#compress;
|
|
561
|
-
#socket;
|
|
562
|
-
#generation = 0;
|
|
563
|
-
#queue = Promise.resolve();
|
|
564
|
-
constructor(options) {
|
|
565
|
-
this.#handlers = options.handlers;
|
|
566
|
-
this.#compress = options.compress ?? false;
|
|
567
|
-
this.#decoder = options.decoder ?? createDecoder(options.compress);
|
|
568
|
-
this.#createSocket = options.createSocket ?? ((url) => new WebSocket(url));
|
|
569
|
-
}
|
|
570
|
-
get connected() {
|
|
571
|
-
return this.#socket?.readyState === OPEN;
|
|
572
|
-
}
|
|
573
|
-
get live() {
|
|
574
|
-
return this.#socket !== undefined && this.#socket.readyState <= OPEN;
|
|
575
|
-
}
|
|
576
|
-
get generation() {
|
|
577
|
-
return this.#generation;
|
|
578
|
-
}
|
|
579
|
-
connect(url) {
|
|
580
|
-
this.#discard(1000, "Superseded by a new connection");
|
|
581
|
-
const generation = ++this.#generation;
|
|
582
|
-
this.#decoder.reset();
|
|
583
|
-
this.#queue = Promise.resolve();
|
|
584
|
-
let socket;
|
|
585
|
-
try {
|
|
586
|
-
socket = this.#createSocket(url);
|
|
587
|
-
} catch (error) {
|
|
588
|
-
return { ok: false, error: this.#asError(error) };
|
|
589
|
-
}
|
|
590
|
-
if (this.#compress) {
|
|
591
|
-
try {
|
|
592
|
-
socket.binaryType = "arraybuffer";
|
|
593
|
-
} catch {}
|
|
594
|
-
}
|
|
595
|
-
this.#socket = socket;
|
|
596
|
-
socket.addEventListener("open", () => {
|
|
597
|
-
if (!this.#owns(generation))
|
|
598
|
-
return;
|
|
599
|
-
this.#handlers.onOpen();
|
|
600
|
-
});
|
|
601
|
-
socket.addEventListener("message", (event) => {
|
|
602
|
-
if (!this.#owns(generation))
|
|
603
|
-
return;
|
|
604
|
-
let decoded;
|
|
605
|
-
try {
|
|
606
|
-
decoded = this.#decoder.push(event.data);
|
|
607
|
-
} catch (error) {
|
|
608
|
-
this.#handlers.onError(this.#asError(error));
|
|
609
|
-
this.#failDecoding(generation);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
if (!(decoded instanceof Promise)) {
|
|
613
|
-
for (const frame of decoded)
|
|
614
|
-
this.#handlers.onFrame(frame);
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
this.#queue = this.#queue.then(async () => {
|
|
618
|
-
const frames = await decoded;
|
|
619
|
-
if (!this.#owns(generation))
|
|
620
|
-
return;
|
|
621
|
-
for (const frame of frames)
|
|
622
|
-
this.#handlers.onFrame(frame);
|
|
623
|
-
}).catch((error) => {
|
|
624
|
-
if (!this.#owns(generation))
|
|
625
|
-
return;
|
|
626
|
-
this.#handlers.onError(this.#asError(error));
|
|
627
|
-
this.#failDecoding(generation);
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
socket.addEventListener("close", (event) => {
|
|
631
|
-
if (!this.#owns(generation))
|
|
632
|
-
return;
|
|
633
|
-
this.#socket = undefined;
|
|
634
|
-
this.#generation++;
|
|
635
|
-
this.#decoder.reset();
|
|
636
|
-
this.#handlers.onClose(event.code, event.reason ?? "");
|
|
637
|
-
});
|
|
638
|
-
socket.addEventListener("error", () => {
|
|
639
|
-
if (!this.#owns(generation))
|
|
640
|
-
return;
|
|
641
|
-
this.#handlers.onError(new Error("Gateway WebSocket error"));
|
|
642
|
-
});
|
|
643
|
-
return { ok: true };
|
|
644
|
-
}
|
|
645
|
-
send(data) {
|
|
646
|
-
if (!this.connected)
|
|
647
|
-
return false;
|
|
648
|
-
try {
|
|
649
|
-
this.#socket.send(data);
|
|
650
|
-
return true;
|
|
651
|
-
} catch (error) {
|
|
652
|
-
this.#handlers.onError(this.#asError(error));
|
|
653
|
-
return false;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
#failDecoding(generation) {
|
|
657
|
-
if (this.#owns(generation))
|
|
658
|
-
this.close(4000, "decoder failure");
|
|
659
|
-
}
|
|
660
|
-
close(code = 1000, reason = "") {
|
|
661
|
-
const socket = this.#socket;
|
|
662
|
-
if (!socket)
|
|
663
|
-
return;
|
|
664
|
-
try {
|
|
665
|
-
socket.close(code, reason);
|
|
666
|
-
} catch (error) {
|
|
667
|
-
this.#handlers.onError(this.#asError(error));
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
destroy(code = 1000, reason = "") {
|
|
671
|
-
this.#discard(code, reason);
|
|
672
|
-
this.#decoder.reset();
|
|
673
|
-
}
|
|
674
|
-
#owns(generation) {
|
|
675
|
-
return generation === this.#generation;
|
|
676
|
-
}
|
|
677
|
-
#discard(code, reason) {
|
|
678
|
-
const socket = this.#socket;
|
|
679
|
-
if (!socket)
|
|
680
|
-
return;
|
|
681
|
-
this.#generation++;
|
|
682
|
-
this.#socket = undefined;
|
|
683
|
-
try {
|
|
684
|
-
socket.close(code, reason);
|
|
685
|
-
} catch (error) {
|
|
686
|
-
this.#handlers.onError(this.#asError(error));
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
#asError(error) {
|
|
690
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// packages/ws/src/opcodes.ts
|
|
695
|
-
var GatewayOpcodes = {
|
|
696
|
-
Dispatch: 0,
|
|
697
|
-
Heartbeat: 1,
|
|
698
|
-
Identify: 2,
|
|
699
|
-
PresenceUpdate: 3,
|
|
700
|
-
VoiceStateUpdate: 4,
|
|
701
|
-
Resume: 6,
|
|
702
|
-
Reconnect: 7,
|
|
703
|
-
RequestGuildMembers: 8,
|
|
704
|
-
InvalidSession: 9,
|
|
705
|
-
Hello: 10,
|
|
706
|
-
HeartbeatAck: 11
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
// packages/ws/src/protocol.ts
|
|
710
|
-
function classifyFrame(raw) {
|
|
711
|
-
let payload;
|
|
712
|
-
try {
|
|
713
|
-
payload = JSON.parse(raw);
|
|
714
|
-
} catch (error) {
|
|
715
|
-
return {
|
|
716
|
-
sequence: null,
|
|
717
|
-
action: {
|
|
718
|
-
type: "invalid",
|
|
719
|
-
violation: "invalid-json",
|
|
720
|
-
message: "Gateway returned invalid JSON",
|
|
721
|
-
cause: error
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
return classifyPayload(payload);
|
|
726
|
-
}
|
|
727
|
-
function classifyPayload(payload) {
|
|
728
|
-
if (payload === null || typeof payload !== "object" || typeof payload.op !== "number")
|
|
729
|
-
return {
|
|
730
|
-
sequence: null,
|
|
731
|
-
action: {
|
|
732
|
-
type: "invalid",
|
|
733
|
-
violation: "invalid-payload",
|
|
734
|
-
message: "Gateway returned an invalid payload"
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
const frame = payload;
|
|
738
|
-
const sequence = typeof frame.s === "number" ? frame.s : null;
|
|
739
|
-
switch (frame.op) {
|
|
740
|
-
case GatewayOpcodes.Dispatch:
|
|
741
|
-
return {
|
|
742
|
-
sequence,
|
|
743
|
-
action: {
|
|
744
|
-
type: "dispatch",
|
|
745
|
-
event: typeof frame.t === "string" ? frame.t : null,
|
|
746
|
-
data: frame.d
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
case GatewayOpcodes.Hello: {
|
|
750
|
-
const interval = frame.d?.heartbeat_interval;
|
|
751
|
-
if (typeof interval !== "number" || !Number.isFinite(interval) || interval <= 0)
|
|
752
|
-
return {
|
|
753
|
-
sequence,
|
|
754
|
-
action: {
|
|
755
|
-
type: "invalid",
|
|
756
|
-
violation: "invalid-hello",
|
|
757
|
-
message: "Gateway HELLO payload contains an invalid heartbeat interval"
|
|
758
|
-
}
|
|
759
|
-
};
|
|
760
|
-
return {
|
|
761
|
-
sequence,
|
|
762
|
-
action: { type: "hello", heartbeatInterval: interval }
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
case GatewayOpcodes.Heartbeat:
|
|
766
|
-
return { sequence, action: { type: "heartbeat" } };
|
|
767
|
-
case GatewayOpcodes.HeartbeatAck:
|
|
768
|
-
return {
|
|
769
|
-
sequence,
|
|
770
|
-
action: { type: "heartbeat-ack", data: frame.d }
|
|
771
|
-
};
|
|
772
|
-
case GatewayOpcodes.Reconnect:
|
|
773
|
-
return { sequence, action: { type: "reconnect" } };
|
|
774
|
-
case GatewayOpcodes.InvalidSession:
|
|
775
|
-
return {
|
|
776
|
-
sequence,
|
|
777
|
-
action: {
|
|
778
|
-
type: "invalid-session",
|
|
779
|
-
resumable: frame.d === true
|
|
780
|
-
}
|
|
781
|
-
};
|
|
782
|
-
default:
|
|
783
|
-
return { sequence, action: { type: "unknown", opcode: frame.op } };
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
function identifyPayload(options) {
|
|
787
|
-
const props = options.properties ?? {};
|
|
788
|
-
const os = props.os ?? process.platform;
|
|
789
|
-
const browser = props.browser ?? "Lunibee";
|
|
790
|
-
const device = props.device ?? "Lunibee";
|
|
791
|
-
return {
|
|
792
|
-
op: GatewayOpcodes.Identify,
|
|
793
|
-
d: {
|
|
794
|
-
token: options.token,
|
|
795
|
-
intents: options.intents,
|
|
796
|
-
properties: {
|
|
797
|
-
os,
|
|
798
|
-
browser,
|
|
799
|
-
device,
|
|
800
|
-
$os: os,
|
|
801
|
-
$browser: browser,
|
|
802
|
-
$device: device,
|
|
803
|
-
...props
|
|
804
|
-
},
|
|
805
|
-
presence: {
|
|
806
|
-
since: options.presence?.since ?? null,
|
|
807
|
-
activities: options.presence?.activities ?? [],
|
|
808
|
-
status: options.presence?.status ?? "online",
|
|
809
|
-
afk: Boolean(options.presence?.afk)
|
|
810
|
-
},
|
|
811
|
-
shard: [options.shardId, options.shardCount]
|
|
812
|
-
},
|
|
813
|
-
s: null,
|
|
814
|
-
t: null
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
function resumePayload(token, resume) {
|
|
818
|
-
return {
|
|
819
|
-
op: GatewayOpcodes.Resume,
|
|
820
|
-
d: {
|
|
821
|
-
token,
|
|
822
|
-
session_id: resume.sessionId,
|
|
823
|
-
seq: resume.sequence
|
|
824
|
-
},
|
|
825
|
-
s: null,
|
|
826
|
-
t: null
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
function heartbeatPayload(sequence) {
|
|
830
|
-
return { op: GatewayOpcodes.Heartbeat, d: sequence, s: null, t: null };
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// packages/types/src/channel-kind.ts
|
|
834
|
-
var ChannelKinds = {
|
|
835
|
-
text: [0],
|
|
836
|
-
dm: [1],
|
|
837
|
-
voice: [2],
|
|
838
|
-
"group-dm": [3],
|
|
839
|
-
category: [4],
|
|
840
|
-
announcement: [5],
|
|
841
|
-
thread: [10, 11, 12],
|
|
842
|
-
stage: [13],
|
|
843
|
-
directory: [14],
|
|
844
|
-
forum: [15],
|
|
845
|
-
media: [16]
|
|
846
|
-
};
|
|
847
|
-
var KIND_OF_TYPE = new Map(Object.keys(ChannelKinds).flatMap((kind) => ChannelKinds[kind].map((type) => [type, kind])));
|
|
848
|
-
// packages/types/src/gateway.ts
|
|
849
|
-
var GatewayIntentBits = {
|
|
850
|
-
Guilds: 1 << 0,
|
|
851
|
-
GuildMembers: 1 << 1,
|
|
852
|
-
GuildModeration: 1 << 2,
|
|
853
|
-
GuildExpressions: 1 << 3,
|
|
854
|
-
GuildIntegrations: 1 << 4,
|
|
855
|
-
GuildWebhooks: 1 << 5,
|
|
856
|
-
GuildInvites: 1 << 6,
|
|
857
|
-
GuildVoiceStates: 1 << 7,
|
|
858
|
-
GuildPresences: 1 << 8,
|
|
859
|
-
GuildMessages: 1 << 9,
|
|
860
|
-
GuildMessageReactions: 1 << 10,
|
|
861
|
-
GuildMessageTyping: 1 << 11,
|
|
862
|
-
DirectMessages: 1 << 12,
|
|
863
|
-
DirectMessageReactions: 1 << 13,
|
|
864
|
-
DirectMessageTyping: 1 << 14,
|
|
865
|
-
MessageContent: 1 << 15,
|
|
866
|
-
GuildScheduledEvents: 1 << 16,
|
|
867
|
-
AutoModerationConfiguration: 1 << 20,
|
|
868
|
-
AutoModerationExecution: 1 << 21,
|
|
869
|
-
GuildMessagePolls: 1 << 24,
|
|
870
|
-
DirectMessagePolls: 1 << 25
|
|
871
|
-
};
|
|
872
|
-
var IntentBits = {
|
|
873
|
-
guild: 1 << 0,
|
|
874
|
-
guilds: 1 << 0,
|
|
875
|
-
guildMembers: 1 << 1,
|
|
876
|
-
guildModeration: 1 << 2,
|
|
877
|
-
guildBans: 1 << 2,
|
|
878
|
-
guildExpressions: 1 << 3,
|
|
879
|
-
guildEmojis: 1 << 3,
|
|
880
|
-
guildEmojisAndStickers: 1 << 3,
|
|
881
|
-
guildIntegrations: 1 << 4,
|
|
882
|
-
guildWebhooks: 1 << 5,
|
|
883
|
-
guildInvites: 1 << 6,
|
|
884
|
-
guildVoiceStates: 1 << 7,
|
|
885
|
-
guildPresences: 1 << 8,
|
|
886
|
-
guildMessages: 1 << 9,
|
|
887
|
-
guildMessage: 1 << 9,
|
|
888
|
-
guildMessageReactions: 1 << 10,
|
|
889
|
-
guildMessageTyping: 1 << 11,
|
|
890
|
-
directMessages: 1 << 12,
|
|
891
|
-
directMessageReactions: 1 << 13,
|
|
892
|
-
directMessageTyping: 1 << 14,
|
|
893
|
-
messageContent: 1 << 15,
|
|
894
|
-
guildScheduledEvents: 1 << 16,
|
|
895
|
-
autoModerationConfiguration: 1 << 20,
|
|
896
|
-
autoModerationExecution: 1 << 21,
|
|
897
|
-
guildMessagePolls: 1 << 24,
|
|
898
|
-
directMessagePolls: 1 << 25
|
|
899
|
-
};
|
|
900
|
-
function resolveGatewayIntents(intents) {
|
|
901
|
-
if (!Array.isArray(intents))
|
|
902
|
-
throw new TypeError("Gateway intents must be an array, e.g. [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages].");
|
|
903
|
-
return intents.reduce((acc, intent) => acc | resolveIntent(intent), 0);
|
|
904
|
-
}
|
|
905
|
-
function resolveIntent(intent) {
|
|
906
|
-
if (typeof intent === "number")
|
|
907
|
-
return intent;
|
|
908
|
-
const bits = {
|
|
909
|
-
...IntentBits,
|
|
910
|
-
...GatewayIntentBits
|
|
911
|
-
};
|
|
912
|
-
const lower = intent.charAt(0).toLowerCase() + intent.slice(1);
|
|
913
|
-
if (Object.hasOwn(bits, intent))
|
|
914
|
-
return bits[intent];
|
|
915
|
-
if (Object.hasOwn(bits, lower))
|
|
916
|
-
return bits[lower];
|
|
917
|
-
const num = Number(intent);
|
|
918
|
-
return Number.isNaN(num) ? 0 : num;
|
|
919
|
-
}
|
|
920
|
-
// packages/ws/src/dispatcher.ts
|
|
921
|
-
class GatewayDispatcher {
|
|
922
|
-
#listeners = new Map;
|
|
923
|
-
#onFailure;
|
|
924
|
-
constructor(onFailure) {
|
|
925
|
-
this.#onFailure = onFailure;
|
|
926
|
-
}
|
|
927
|
-
on(event, listener) {
|
|
928
|
-
let listeners = this.#listeners.get(event);
|
|
929
|
-
if (!listeners)
|
|
930
|
-
this.#listeners.set(event, listeners = new Set);
|
|
931
|
-
listeners.add(listener);
|
|
932
|
-
}
|
|
933
|
-
off(event, listener) {
|
|
934
|
-
this.#listeners.get(event)?.delete(listener);
|
|
935
|
-
}
|
|
936
|
-
emit(event, data) {
|
|
937
|
-
for (const listener of this.#listeners.get(event) ?? []) {
|
|
938
|
-
try {
|
|
939
|
-
const result = listener(data);
|
|
940
|
-
if (result && typeof result.then === "function")
|
|
941
|
-
Promise.resolve(result).catch((error) => {
|
|
942
|
-
if (event !== "error")
|
|
943
|
-
this.#onFailure(error);
|
|
944
|
-
});
|
|
945
|
-
} catch (error) {
|
|
946
|
-
if (event !== "error")
|
|
947
|
-
this.#onFailure(error);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
// packages/ws/src/send-budget.ts
|
|
953
|
-
class GatewaySendLimiter {
|
|
954
|
-
#timestamps = [];
|
|
955
|
-
#limit;
|
|
956
|
-
#windowMs;
|
|
957
|
-
constructor(limit = 115, windowMs = 60000) {
|
|
958
|
-
this.#limit = limit;
|
|
959
|
-
this.#windowMs = windowMs;
|
|
960
|
-
}
|
|
961
|
-
get used() {
|
|
962
|
-
return this.#timestamps.length;
|
|
963
|
-
}
|
|
964
|
-
allows(privileged, now = Date.now()) {
|
|
965
|
-
while (this.#timestamps.length && now - this.#timestamps[0] >= this.#windowMs)
|
|
966
|
-
this.#timestamps.shift();
|
|
967
|
-
return privileged || this.#timestamps.length < this.#limit;
|
|
968
|
-
}
|
|
969
|
-
record(now = Date.now()) {
|
|
970
|
-
this.#timestamps.push(now);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
// packages/ws/src/state.ts
|
|
974
|
-
class GatewayError extends Error {
|
|
975
|
-
code;
|
|
976
|
-
constructor(message, code, options) {
|
|
977
|
-
super(message, options);
|
|
978
|
-
this.name = "GatewayError";
|
|
979
|
-
this.code = code;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
// packages/ws/src/index.ts
|
|
983
|
-
var RESUMABLE_CLOSE_CODE = 4900;
|
|
984
|
-
var DEFAULT_GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
985
|
-
|
|
986
|
-
class Gateway {
|
|
987
|
-
state = "CONNECT" /* Connect */;
|
|
988
|
-
#options;
|
|
989
|
-
#transport;
|
|
990
|
-
#session = new GatewaySession;
|
|
991
|
-
#token = 0;
|
|
992
|
-
#heartbeat;
|
|
993
|
-
#closed = false;
|
|
994
|
-
#reconnect;
|
|
995
|
-
#events = new GatewayDispatcher((error) => this.#emitError(error));
|
|
996
|
-
#sendBudget = new GatewaySendLimiter;
|
|
997
|
-
#reconnects = 0;
|
|
998
|
-
ping = -1;
|
|
999
|
-
constructor(options) {
|
|
1000
|
-
if (!options.token?.trim())
|
|
1001
|
-
throw new TypeError("A Gateway token is required.");
|
|
1002
|
-
for (const key of [
|
|
1003
|
-
"reconnectBaseDelay",
|
|
1004
|
-
"reconnectMaxDelay",
|
|
1005
|
-
"connectTimeout"
|
|
1006
|
-
])
|
|
1007
|
-
if (options[key] !== undefined && !(options[key] > 0))
|
|
1008
|
-
throw new RangeError(`Gateway ${key} must be greater than 0.`);
|
|
1009
|
-
const resolvedIntents = resolveGatewayIntents(options.intents);
|
|
1010
|
-
if (!Number.isInteger(resolvedIntents) || resolvedIntents < 0)
|
|
1011
|
-
throw new TypeError("Gateway intents must be a non-negative integer.");
|
|
1012
|
-
this.#options = {
|
|
1013
|
-
shardId: 0,
|
|
1014
|
-
shardCount: 1,
|
|
1015
|
-
reconnect: true,
|
|
1016
|
-
maxReconnectAttempts: Infinity,
|
|
1017
|
-
connectTimeout: 30000,
|
|
1018
|
-
reconnectBaseDelay: 1000,
|
|
1019
|
-
reconnectMaxDelay: 30000,
|
|
1020
|
-
heartbeatAckTimeout: 1e4,
|
|
1021
|
-
zombieTimeout: 30000,
|
|
1022
|
-
properties: {
|
|
1023
|
-
os: process.platform,
|
|
1024
|
-
browser: "Lunibee",
|
|
1025
|
-
device: "Lunibee"
|
|
1026
|
-
},
|
|
1027
|
-
presence: {
|
|
1028
|
-
status: "online",
|
|
1029
|
-
activities: [],
|
|
1030
|
-
afk: false,
|
|
1031
|
-
since: null
|
|
1032
|
-
},
|
|
1033
|
-
...options,
|
|
1034
|
-
intents: resolvedIntents
|
|
1035
|
-
};
|
|
1036
|
-
if (this.#options.shardId < 0 || this.#options.shardId >= this.#options.shardCount)
|
|
1037
|
-
throw new RangeError("Gateway shardId must be within the configured shard count.");
|
|
1038
|
-
if (this.#options.zombieTimeout <= this.#options.heartbeatAckTimeout)
|
|
1039
|
-
throw new RangeError("Gateway zombieTimeout must be greater than heartbeatAckTimeout");
|
|
1040
|
-
this.#transport = new WebSocketTransport({
|
|
1041
|
-
compress: this.#options.compress,
|
|
1042
|
-
createSocket: options.createSocket,
|
|
1043
|
-
handlers: {
|
|
1044
|
-
onOpen: () => this.#onOpen(),
|
|
1045
|
-
onFrame: (raw) => this.#message(raw, this.#token),
|
|
1046
|
-
onClose: (code) => this.#close(code, this.#token),
|
|
1047
|
-
onError: (error) => this.#emitError(error instanceof GatewayError ? error : new GatewayError(error.message, undefined, {
|
|
1048
|
-
cause: error
|
|
1049
|
-
}))
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
1052
|
-
this.#reconnect = new GatewayReconnect({
|
|
1053
|
-
enabled: this.#options.reconnect,
|
|
1054
|
-
maxAttempts: this.#options.maxReconnectAttempts,
|
|
1055
|
-
baseDelay: this.#options.reconnectBaseDelay,
|
|
1056
|
-
maxDelay: this.#options.reconnectMaxDelay,
|
|
1057
|
-
stormLimit: options.reconnectStormLimit,
|
|
1058
|
-
stormWindow: options.reconnectStormWindow
|
|
1059
|
-
});
|
|
1060
|
-
this.#heartbeat = new GatewayHeartbeat({
|
|
1061
|
-
ackTimeout: this.#options.heartbeatAckTimeout,
|
|
1062
|
-
zombieTimeout: this.#options.zombieTimeout,
|
|
1063
|
-
send: (sequence) => this.#dispatch(heartbeatPayload(sequence), true),
|
|
1064
|
-
sequence: () => this.#session.sequence,
|
|
1065
|
-
isConnected: () => !this.#closed && this.#transport.connected,
|
|
1066
|
-
onTimeout: (timeout) => this.#onHeartbeatTimeout(timeout),
|
|
1067
|
-
onError: (error) => this.#emitError(error)
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
#onHeartbeatTimeout(timeout) {
|
|
1071
|
-
if (timeout.type === "zombie") {
|
|
1072
|
-
this.#emit("zombie", {
|
|
1073
|
-
silentFor: timeout.silentFor,
|
|
1074
|
-
timeout: timeout.deadline
|
|
1075
|
-
});
|
|
1076
|
-
this.#emitError(new GatewayError(`Gateway connection appears stale after ${timeout.silentFor}ms without traffic.`));
|
|
1077
|
-
} else {
|
|
1078
|
-
this.#emitError(new GatewayError(`Gateway heartbeat acknowledgement timed out after ${timeout.elapsedMs}ms.`));
|
|
1079
|
-
}
|
|
1080
|
-
this.#transport.close(RESUMABLE_CLOSE_CODE, timeout.type === "zombie" ? "Zombie Gateway connection" : "Heartbeat timeout");
|
|
1081
|
-
}
|
|
1082
|
-
connect(url = DEFAULT_GATEWAY_URL) {
|
|
1083
|
-
if (this.state === "CLOSED" /* Closed */)
|
|
1084
|
-
return Promise.reject(new GatewayError("Gateway has been permanently closed."));
|
|
1085
|
-
const inFlight = this.#reconnect.attemptPromise;
|
|
1086
|
-
if (inFlight)
|
|
1087
|
-
return inFlight;
|
|
1088
|
-
if (this.#transport.live)
|
|
1089
|
-
return Promise.resolve();
|
|
1090
|
-
this.#reconnect.cancel();
|
|
1091
|
-
this.#closed = false;
|
|
1092
|
-
this.#setState("CONNECT" /* Connect */);
|
|
1093
|
-
const connectURL = this.#options.compress ? url.includes("compress=") ? url : `${url}${url.includes("?") ? "&" : "?"}compress=zlib-stream` : url;
|
|
1094
|
-
return this.#reconnect.attempt(() => this.#open(connectURL));
|
|
1095
|
-
}
|
|
1096
|
-
close() {
|
|
1097
|
-
this.#closed = true;
|
|
1098
|
-
this.#clearTimers();
|
|
1099
|
-
this.#settleConnect(new GatewayError("Gateway connection closed before socket open."));
|
|
1100
|
-
this.#transport.destroy(1000, "Client closed connection");
|
|
1101
|
-
this.#setState("CLOSED" /* Closed */);
|
|
1102
|
-
}
|
|
1103
|
-
emit(event, data) {
|
|
1104
|
-
this.#emit(event, data);
|
|
1105
|
-
}
|
|
1106
|
-
on(event, listener) {
|
|
1107
|
-
if (!event || typeof listener !== "function")
|
|
1108
|
-
throw new TypeError("Gateway event and listener are required.");
|
|
1109
|
-
this.#events.on(event, listener);
|
|
1110
|
-
return this;
|
|
1111
|
-
}
|
|
1112
|
-
off(event, listener) {
|
|
1113
|
-
this.#events.off(event, listener);
|
|
1114
|
-
return this;
|
|
1115
|
-
}
|
|
1116
|
-
health() {
|
|
1117
|
-
return {
|
|
1118
|
-
state: this.state,
|
|
1119
|
-
latency: this.#heartbeat.latency,
|
|
1120
|
-
silentFor: this.#heartbeat.silentFor,
|
|
1121
|
-
reconnects: this.#reconnects
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
send(payload) {
|
|
1125
|
-
if (!payload || !Number.isInteger(payload.op))
|
|
1126
|
-
throw new TypeError("Gateway payload must contain an integer opcode.");
|
|
1127
|
-
return this.#dispatch(payload, false);
|
|
1128
|
-
}
|
|
1129
|
-
#dispatch(payload, privileged) {
|
|
1130
|
-
if (!this.#transport.connected)
|
|
1131
|
-
return false;
|
|
1132
|
-
const now = Date.now();
|
|
1133
|
-
if (!this.#sendBudget.allows(privileged, now)) {
|
|
1134
|
-
this.#emitError(new GatewayError("Gateway send rate budget exhausted."));
|
|
1135
|
-
return false;
|
|
1136
|
-
}
|
|
1137
|
-
if (!this.#transport.send(JSON.stringify(payload)))
|
|
1138
|
-
return false;
|
|
1139
|
-
this.#sendBudget.record(now);
|
|
1140
|
-
return true;
|
|
1141
|
-
}
|
|
1142
|
-
setPresence(data) {
|
|
1143
|
-
this.#options.presence = { ...this.#options.presence, ...data };
|
|
1144
|
-
const payload = {
|
|
1145
|
-
since: this.#options.presence.since ?? null,
|
|
1146
|
-
activities: this.#options.presence.activities ?? [],
|
|
1147
|
-
status: this.#options.presence.status ?? "online",
|
|
1148
|
-
afk: Boolean(this.#options.presence.afk)
|
|
1149
|
-
};
|
|
1150
|
-
return this.send({
|
|
1151
|
-
op: GatewayOpcodes.PresenceUpdate,
|
|
1152
|
-
d: payload,
|
|
1153
|
-
s: null,
|
|
1154
|
-
t: null
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
setVoiceState(data) {
|
|
1158
|
-
return this.send({
|
|
1159
|
-
op: GatewayOpcodes.VoiceStateUpdate,
|
|
1160
|
-
d: data,
|
|
1161
|
-
s: null,
|
|
1162
|
-
t: null
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
sendVoiceState(guildId, channelId, options = {}) {
|
|
1166
|
-
if (!this.ownsGuild(guildId))
|
|
1167
|
-
return false;
|
|
1168
|
-
return this.setVoiceState({
|
|
1169
|
-
guild_id: guildId,
|
|
1170
|
-
channel_id: channelId,
|
|
1171
|
-
self_mute: options.selfMute ?? false,
|
|
1172
|
-
self_deaf: options.selfDeaf ?? false
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
ownsGuild(guildId) {
|
|
1176
|
-
const count = this.#options.shardCount;
|
|
1177
|
-
if (count <= 1)
|
|
1178
|
-
return true;
|
|
1179
|
-
if (!/^\d{1,20}$/.test(guildId))
|
|
1180
|
-
return false;
|
|
1181
|
-
return Number((BigInt(guildId) >> 22n) % BigInt(count)) === this.#options.shardId;
|
|
1182
|
-
}
|
|
1183
|
-
requestGuildMembers(data) {
|
|
1184
|
-
return this.send({
|
|
1185
|
-
op: GatewayOpcodes.RequestGuildMembers,
|
|
1186
|
-
d: data,
|
|
1187
|
-
s: null,
|
|
1188
|
-
t: null
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
#open(url) {
|
|
1192
|
-
if (this.#closed)
|
|
1193
|
-
return;
|
|
1194
|
-
this.#token = this.#session.beginConnection();
|
|
1195
|
-
const result = this.#transport.connect(url);
|
|
1196
|
-
if (result.ok) {
|
|
1197
|
-
const token = this.#token;
|
|
1198
|
-
clearTimeout(this.#connectTimer);
|
|
1199
|
-
this.#connectTimer = setTimeout(() => {
|
|
1200
|
-
if (token === this.#token && !this.#closed)
|
|
1201
|
-
this.#transport.close(4000, "connect timeout");
|
|
1202
|
-
}, this.#options.connectTimeout);
|
|
1203
|
-
this.#connectTimer.unref?.();
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
const failure = this.#normalizeError(result.error);
|
|
1207
|
-
this.#emitError(failure);
|
|
1208
|
-
if (this.#options.reconnect) {
|
|
1209
|
-
this.#setState("RECONNECT" /* Reconnect */);
|
|
1210
|
-
this.#scheduleReconnect();
|
|
1211
|
-
} else {
|
|
1212
|
-
this.#settleConnect(failure);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
#connectTimer;
|
|
1216
|
-
#onOpen() {
|
|
1217
|
-
clearTimeout(this.#connectTimer);
|
|
1218
|
-
this.#heartbeat.watch();
|
|
1219
|
-
this.#emit("open", undefined);
|
|
1220
|
-
this.#settleConnect();
|
|
1221
|
-
}
|
|
1222
|
-
#message(raw, token) {
|
|
1223
|
-
if (!this.#session.owns(token))
|
|
1224
|
-
return;
|
|
1225
|
-
this.#heartbeat.receivedMessage();
|
|
1226
|
-
const { sequence, action } = classifyFrame(raw);
|
|
1227
|
-
if (sequence !== null)
|
|
1228
|
-
this.#session.recordSequence(sequence, token);
|
|
1229
|
-
this.#apply(action, token);
|
|
1230
|
-
}
|
|
1231
|
-
#apply(action, token) {
|
|
1232
|
-
switch (action.type) {
|
|
1233
|
-
case "dispatch":
|
|
1234
|
-
this.#setState("DISPATCH" /* Dispatch */);
|
|
1235
|
-
this.#handleDispatch(action.event, action.data, token);
|
|
1236
|
-
return;
|
|
1237
|
-
case "hello":
|
|
1238
|
-
this.#handleHello(action.heartbeatInterval);
|
|
1239
|
-
return;
|
|
1240
|
-
case "heartbeat":
|
|
1241
|
-
this.#setState("HEARTBEAT" /* Heartbeat */);
|
|
1242
|
-
this.#heartbeat.sendHeartbeat();
|
|
1243
|
-
return;
|
|
1244
|
-
case "heartbeat-ack":
|
|
1245
|
-
this.#heartbeat.acknowledge();
|
|
1246
|
-
this.ping = this.#heartbeat.latency;
|
|
1247
|
-
this.#emit("heartbeatAck", action.data);
|
|
1248
|
-
return;
|
|
1249
|
-
case "reconnect":
|
|
1250
|
-
this.#transport.close(RESUMABLE_CLOSE_CODE, "Server requested reconnect");
|
|
1251
|
-
return;
|
|
1252
|
-
case "invalid-session": {
|
|
1253
|
-
if (!action.resumable)
|
|
1254
|
-
this.#session.invalidate(token, "invalid-session");
|
|
1255
|
-
this.#emit("invalidSession", action.resumable);
|
|
1256
|
-
this.#transport.close(action.resumable ? GatewayCloseCodes.UnknownError : 1000, action.resumable ? "Invalid session (resumable)" : "Invalid session");
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
case "invalid":
|
|
1260
|
-
this.#emitError(new GatewayError(action.message, undefined, action.cause === undefined ? undefined : { cause: action.cause }));
|
|
1261
|
-
this.#transport.close(1002, action.violation === "invalid-json" ? "Invalid JSON" : action.violation === "invalid-hello" ? "Invalid heartbeat interval" : "Invalid payload");
|
|
1262
|
-
return;
|
|
1263
|
-
case "unknown":
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
#handleDispatch(event, data, token) {
|
|
1268
|
-
if (event === "READY") {
|
|
1269
|
-
if (!this.#session.activate(data, token)) {
|
|
1270
|
-
if (this.#session.owns(token))
|
|
1271
|
-
this.#emitError(new GatewayError("Gateway READY payload is missing session information"));
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
this.#reconnect.reset();
|
|
1275
|
-
this.#setState("READY" /* Ready */);
|
|
1276
|
-
this.#emit("ready", data);
|
|
1277
|
-
} else if (event === "RESUMED") {
|
|
1278
|
-
this.#reconnect.reset();
|
|
1279
|
-
this.#setState("READY" /* Ready */);
|
|
1280
|
-
this.#emit("resumed", data);
|
|
1281
|
-
}
|
|
1282
|
-
this.#emit("RAW", { event: event ?? "UNKNOWN", data });
|
|
1283
|
-
if (this.#closed)
|
|
1284
|
-
return;
|
|
1285
|
-
if (event == null || /^[A-Z][A-Z0-9_]*$/.test(event))
|
|
1286
|
-
this.#emit(event ?? "dispatch", data);
|
|
1287
|
-
}
|
|
1288
|
-
#handleHello(interval) {
|
|
1289
|
-
this.#setState(this.#session.canResume ? "RESUME" /* Resume */ : "HELLO" /* Hello */);
|
|
1290
|
-
this.#heartbeat.start(interval);
|
|
1291
|
-
this.#identifyOrResume();
|
|
1292
|
-
}
|
|
1293
|
-
#identifyOrResume() {
|
|
1294
|
-
const intent = this.#session.handshake();
|
|
1295
|
-
if (intent.type === "resume") {
|
|
1296
|
-
this.#setState("RESUME" /* Resume */);
|
|
1297
|
-
this.#dispatch(resumePayload(this.#options.token, intent), true);
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
this.#setState("IDENTIFY" /* Identify */);
|
|
1301
|
-
this.#dispatch(identifyPayload({
|
|
1302
|
-
token: this.#options.token,
|
|
1303
|
-
intents: this.#options.intents,
|
|
1304
|
-
shardId: this.#options.shardId,
|
|
1305
|
-
shardCount: this.#options.shardCount,
|
|
1306
|
-
properties: this.#options.properties,
|
|
1307
|
-
presence: this.#options.presence
|
|
1308
|
-
}), true);
|
|
1309
|
-
}
|
|
1310
|
-
#close(code, token) {
|
|
1311
|
-
this.#clearTimers();
|
|
1312
|
-
const action = this.#reconnect.classifyClose(code, this.#session.canResume);
|
|
1313
|
-
this.#emit("close", { code, action });
|
|
1314
|
-
if (this.#closed || !this.#reconnect.enabled || action === "stop") {
|
|
1315
|
-
if (action === "stop")
|
|
1316
|
-
this.#closed = true;
|
|
1317
|
-
this.#setState(this.#closed ? "CLOSED" /* Closed */ : "CONNECT" /* Connect */);
|
|
1318
|
-
if (this.#reconnect.connecting)
|
|
1319
|
-
this.#reconnect.settle(new GatewayError(`Gateway closed before READY (code ${code}).`, code));
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
if (action === "identify")
|
|
1323
|
-
this.#session.invalidate(token, "session-timeout");
|
|
1324
|
-
this.#setState("RECONNECT" /* Reconnect */);
|
|
1325
|
-
this.#scheduleReconnect();
|
|
1326
|
-
}
|
|
1327
|
-
#scheduleReconnect() {
|
|
1328
|
-
if (this.#closed)
|
|
1329
|
-
return;
|
|
1330
|
-
const result = this.#reconnect.schedule(() => this.#open(this.#session.connectURL(DEFAULT_GATEWAY_URL)));
|
|
1331
|
-
if (result.scheduled) {
|
|
1332
|
-
this.#reconnects++;
|
|
1333
|
-
if (result.unstable)
|
|
1334
|
-
this.#emit("unstable", {
|
|
1335
|
-
reconnects: result.unstable.reconnects,
|
|
1336
|
-
delay: result.delayMs
|
|
1337
|
-
});
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
if (result.reason !== "exhausted")
|
|
1341
|
-
return;
|
|
1342
|
-
this.#reconnect.settle(new GatewayError("Gateway reconnect attempts exhausted."));
|
|
1343
|
-
this.#setState("CONNECT" /* Connect */);
|
|
1344
|
-
}
|
|
1345
|
-
#settleConnect(error) {
|
|
1346
|
-
this.#reconnect.settle(error);
|
|
1347
|
-
}
|
|
1348
|
-
#clearTimers() {
|
|
1349
|
-
clearTimeout(this.#connectTimer);
|
|
1350
|
-
this.#reconnect.cancel();
|
|
1351
|
-
this.#heartbeat.stop();
|
|
1352
|
-
}
|
|
1353
|
-
#normalizeError(error) {
|
|
1354
|
-
return error instanceof GatewayError ? error : new GatewayError("Gateway connection failed.", undefined, {
|
|
1355
|
-
cause: error
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
#emitError(error) {
|
|
1359
|
-
this.#emit("error", error instanceof Error ? error : new GatewayError(String(error)));
|
|
1360
|
-
}
|
|
1361
|
-
#emit(event, data) {
|
|
1362
|
-
this.#events.emit(event, data);
|
|
1363
|
-
}
|
|
1364
|
-
#setState(next) {
|
|
1365
|
-
if (this.state === next)
|
|
1366
|
-
return;
|
|
1367
|
-
const previous = this.state;
|
|
1368
|
-
this.state = next;
|
|
1369
|
-
this.#emit("stateChange", { previous, next });
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
// packages/sharding/src/context.ts
|
|
1374
|
-
var WORKER_MARKER = "LUNIBEE_CLUSTER_WORKER";
|
|
1375
|
-
function isClusterWorker(env = process.env) {
|
|
1376
|
-
return env[WORKER_MARKER] === "1";
|
|
1377
|
-
}
|
|
1378
|
-
function validateShardIds(shardIds, total) {
|
|
1379
|
-
if (!Number.isInteger(total) || total < 1)
|
|
1380
|
-
throw new RangeError("Shard count must be a positive integer.");
|
|
1381
|
-
const seen = new Set;
|
|
1382
|
-
for (const id of shardIds) {
|
|
1383
|
-
if (!Number.isInteger(id) || id < 0 || id >= total)
|
|
1384
|
-
throw new RangeError(`Shard ID ${String(id)} is outside 0-${total - 1}.`);
|
|
1385
|
-
if (seen.has(id))
|
|
1386
|
-
throw new RangeError(`Shard ID ${id} is listed twice.`);
|
|
1387
|
-
seen.add(id);
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
function allocateShards(shardIds, clusterCount) {
|
|
1391
|
-
if (!Number.isInteger(clusterCount) || clusterCount < 1)
|
|
1392
|
-
throw new RangeError("clusterCount must be a positive integer.");
|
|
1393
|
-
const chunks = Array.from({ length: clusterCount }, () => []);
|
|
1394
|
-
shardIds.forEach((id, index) => chunks[index % clusterCount].push(id));
|
|
1395
|
-
return chunks;
|
|
1396
|
-
}
|
|
1397
|
-
function assertCompleteAllocation(chunks, expected) {
|
|
1398
|
-
const owners = new Map;
|
|
1399
|
-
const duplicate = [];
|
|
1400
|
-
chunks.forEach((chunk, cluster) => {
|
|
1401
|
-
for (const id of chunk) {
|
|
1402
|
-
if (owners.has(id))
|
|
1403
|
-
duplicate.push(id);
|
|
1404
|
-
owners.set(id, cluster);
|
|
1405
|
-
}
|
|
1406
|
-
});
|
|
1407
|
-
const want = new Set(expected);
|
|
1408
|
-
const missing = expected.filter((id) => !owners.has(id));
|
|
1409
|
-
const extra = [...owners.keys()].filter((id) => !want.has(id));
|
|
1410
|
-
if (duplicate.length || missing.length || extra.length)
|
|
1411
|
-
throw new Error(`Shard allocation is not exact: duplicate [${duplicate}], missing [${missing}], unexpected [${extra}].`);
|
|
1412
|
-
}
|
|
1413
|
-
function parseInteger(name, value) {
|
|
1414
|
-
if (value === undefined || !/^\d+$/.test(value))
|
|
1415
|
-
throw new RangeError(`${name} must be a non-negative integer.`);
|
|
1416
|
-
return Number(value);
|
|
1417
|
-
}
|
|
1418
|
-
function readClusterContext(env = process.env) {
|
|
1419
|
-
if (!isClusterWorker(env))
|
|
1420
|
-
return null;
|
|
1421
|
-
const shardCount = parseInteger("SHARD_COUNT", env.SHARD_COUNT);
|
|
1422
|
-
const shardIds = (env.SHARD_LIST ?? "").split(",").filter((part) => part !== "").map((part) => parseInteger("SHARD_LIST", part));
|
|
1423
|
-
validateShardIds(shardIds, shardCount);
|
|
1424
|
-
return {
|
|
1425
|
-
clusterId: parseInteger("CLUSTER_ID", env.CLUSTER_ID),
|
|
1426
|
-
clusterCount: parseInteger("CLUSTER_COUNT", env.CLUSTER_COUNT),
|
|
1427
|
-
shardCount,
|
|
1428
|
-
shardIds,
|
|
1429
|
-
coordinatedIdentify: env.LUNIBEE_IDENTIFY === "coordinated"
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// packages/sharding/src/protocol.ts
|
|
1434
|
-
var CLUSTER_FRAME = "__lunibeeCluster";
|
|
1435
|
-
var PARENT_ID = -1;
|
|
1436
|
-
var REQUEST_HEALTH = "__lunibee:health";
|
|
1437
|
-
var REQUEST_ASSIGN = "__lunibee:assign";
|
|
1438
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
1439
|
-
var isShardId = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
1440
|
-
function parseChildFrame(value) {
|
|
1441
|
-
if (!isRecord(value))
|
|
1442
|
-
return null;
|
|
1443
|
-
switch (value.kind) {
|
|
1444
|
-
case "ready":
|
|
1445
|
-
return Array.isArray(value.shardIds) && value.shardIds.every(isShardId) ? { kind: "ready", shardIds: value.shardIds } : null;
|
|
1446
|
-
case "identify":
|
|
1447
|
-
return typeof value.id === "number" && isShardId(value.shardId) ? { kind: "identify", id: value.id, shardId: value.shardId } : null;
|
|
1448
|
-
case "identify-done":
|
|
1449
|
-
return isShardId(value.shardId) ? { kind: "identify-done", shardId: value.shardId } : null;
|
|
1450
|
-
case "reply":
|
|
1451
|
-
return typeof value.id === "string" && (value.error === undefined || typeof value.error === "string") ? {
|
|
1452
|
-
kind: "reply",
|
|
1453
|
-
id: value.id,
|
|
1454
|
-
...value.error !== undefined ? { error: value.error } : { result: value.result }
|
|
1455
|
-
} : null;
|
|
1456
|
-
case "health":
|
|
1457
|
-
return isRecord(value.report) && Array.isArray(value.report.shards) && typeof value.report.reportedAt === "number" ? { kind: "health", report: value.report } : null;
|
|
1458
|
-
default:
|
|
1459
|
-
return null;
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
function parseParentFrame(value) {
|
|
1463
|
-
if (!isRecord(value))
|
|
1464
|
-
return null;
|
|
1465
|
-
switch (value.kind) {
|
|
1466
|
-
case "identify-grant":
|
|
1467
|
-
return typeof value.id === "number" ? { kind: "identify-grant", id: value.id } : null;
|
|
1468
|
-
case "identify-deny":
|
|
1469
|
-
return typeof value.id === "number" && typeof value.error === "string" ? { kind: "identify-deny", id: value.id, error: value.error } : null;
|
|
1470
|
-
case "request":
|
|
1471
|
-
return typeof value.id === "string" && typeof value.type === "string" ? {
|
|
1472
|
-
kind: "request",
|
|
1473
|
-
id: value.id,
|
|
1474
|
-
type: value.type,
|
|
1475
|
-
data: value.data
|
|
1476
|
-
} : null;
|
|
1477
|
-
default:
|
|
1478
|
-
return null;
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
// packages/sharding/src/thread.ts
|
|
1483
|
-
import { EventEmitter } from "events";
|
|
2
|
+
import"../chunks/index-fdvgk2sw.js";
|
|
3
|
+
import"../chunks/index-gyvps8b9.js";
|
|
1484
4
|
import {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
send(frame) {
|
|
1517
|
-
if (!this.connected)
|
|
1518
|
-
return false;
|
|
1519
|
-
try {
|
|
1520
|
-
this.#worker.postMessage(frame);
|
|
1521
|
-
return true;
|
|
1522
|
-
} catch {
|
|
1523
|
-
return false;
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
kill(signal = "SIGTERM") {
|
|
1527
|
-
if (!this.connected)
|
|
1528
|
-
return false;
|
|
1529
|
-
this.#signal ??= signal;
|
|
1530
|
-
this.#worker.terminate();
|
|
1531
|
-
return true;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
function threadFork(options = {}) {
|
|
1535
|
-
return (script, env) => {
|
|
1536
|
-
const defined = {};
|
|
1537
|
-
for (const [key, value] of Object.entries(env))
|
|
1538
|
-
if (value !== undefined)
|
|
1539
|
-
defined[key] = value;
|
|
1540
|
-
const worker = new Worker(script, {
|
|
1541
|
-
env: defined,
|
|
1542
|
-
workerData: { [THREAD_MARKER]: true },
|
|
1543
|
-
...options.resourceLimits ? { resourceLimits: options.resourceLimits } : {}
|
|
1544
|
-
});
|
|
1545
|
-
return new ThreadChild(worker);
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
var endpoint;
|
|
1549
|
-
function threadEndpoint() {
|
|
1550
|
-
const port = parentPort;
|
|
1551
|
-
if (isMainThread || !port || !workerData?.[THREAD_MARKER])
|
|
1552
|
-
return;
|
|
1553
|
-
endpoint ??= portEndpoint(port);
|
|
1554
|
-
return endpoint;
|
|
1555
|
-
}
|
|
1556
|
-
function portEndpoint(port) {
|
|
1557
|
-
const wrapped = {
|
|
1558
|
-
send: (message) => {
|
|
1559
|
-
port.postMessage(message);
|
|
1560
|
-
return true;
|
|
1561
|
-
},
|
|
1562
|
-
on: (event, listener) => {
|
|
1563
|
-
if (event === "message")
|
|
1564
|
-
port.on("message", listener);
|
|
1565
|
-
return wrapped;
|
|
1566
|
-
},
|
|
1567
|
-
off: (event, listener) => {
|
|
1568
|
-
if (event === "message")
|
|
1569
|
-
port.off("message", listener);
|
|
1570
|
-
return wrapped;
|
|
1571
|
-
}
|
|
1572
|
-
};
|
|
1573
|
-
return wrapped;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
// packages/sharding/src/worker.ts
|
|
1577
|
-
class ClusterWorker {
|
|
1578
|
-
static #current;
|
|
1579
|
-
static current() {
|
|
1580
|
-
if (!ClusterWorker.#current) {
|
|
1581
|
-
const context = readClusterContext();
|
|
1582
|
-
const endpoint = typeof process.send === "function" ? process : threadEndpoint();
|
|
1583
|
-
if (context && endpoint)
|
|
1584
|
-
ClusterWorker.#current = new ClusterWorker(context, endpoint);
|
|
1585
|
-
}
|
|
1586
|
-
return ClusterWorker.#current;
|
|
1587
|
-
}
|
|
1588
|
-
static reset() {
|
|
1589
|
-
ClusterWorker.#current?.close();
|
|
1590
|
-
ClusterWorker.#current = undefined;
|
|
1591
|
-
}
|
|
1592
|
-
context;
|
|
1593
|
-
#endpoint;
|
|
1594
|
-
#handlers = new Map;
|
|
1595
|
-
#waiting = new Map;
|
|
1596
|
-
#onMessage;
|
|
1597
|
-
#onDisconnect;
|
|
1598
|
-
#counter = 0;
|
|
1599
|
-
#closed = false;
|
|
1600
|
-
constructor(context, endpoint = process, options = {}) {
|
|
1601
|
-
if (typeof endpoint.send !== "function")
|
|
1602
|
-
throw new Error("ClusterWorker needs an IPC channel: run it in a process forked by ClusterManager.");
|
|
1603
|
-
this.context = context;
|
|
1604
|
-
this.#endpoint = endpoint;
|
|
1605
|
-
this.#onMessage = (frame) => this.#handle(frame);
|
|
1606
|
-
const exitOnLoss = options.exitOnParentLoss ?? endpoint === process;
|
|
1607
|
-
const exit = options.exit ?? ((code) => process.exit(code));
|
|
1608
|
-
this.#onDisconnect = () => {
|
|
1609
|
-
this.#failAll("The parent went away.");
|
|
1610
|
-
if (exitOnLoss && !this.#closed)
|
|
1611
|
-
exit(1);
|
|
1612
|
-
};
|
|
1613
|
-
endpoint.on("message", this.#onMessage);
|
|
1614
|
-
endpoint.on("disconnect", this.#onDisconnect);
|
|
1615
|
-
}
|
|
1616
|
-
get identifyGate() {
|
|
1617
|
-
return this.context.coordinatedIdentify ? this.#gate : undefined;
|
|
1618
|
-
}
|
|
1619
|
-
#gate = {
|
|
1620
|
-
acquire: (shardId) => {
|
|
1621
|
-
if (this.#closed)
|
|
1622
|
-
return Promise.reject(new Error("The cluster worker is closed."));
|
|
1623
|
-
const id = ++this.#counter;
|
|
1624
|
-
return new Promise((resolve, reject) => {
|
|
1625
|
-
this.#waiting.set(id, { resolve, reject });
|
|
1626
|
-
this.#send({ kind: "identify", id, shardId });
|
|
1627
|
-
});
|
|
1628
|
-
},
|
|
1629
|
-
release: (shardId) => this.#send({ kind: "identify-done", shardId })
|
|
1630
|
-
};
|
|
1631
|
-
ready(shardIds = this.context.shardIds) {
|
|
1632
|
-
this.#send({ kind: "ready", shardIds: [...shardIds] });
|
|
1633
|
-
}
|
|
1634
|
-
reportHealth(report) {
|
|
1635
|
-
this.#send({ kind: "health", report });
|
|
1636
|
-
}
|
|
1637
|
-
onRequest(type, handler) {
|
|
1638
|
-
if (!type.trim() || typeof handler !== "function")
|
|
1639
|
-
throw new TypeError("A request type and handler are required.");
|
|
1640
|
-
this.#handlers.set(type, handler);
|
|
1641
|
-
return this;
|
|
1642
|
-
}
|
|
1643
|
-
close() {
|
|
1644
|
-
this.#closed = true;
|
|
1645
|
-
this.#endpoint.off("message", this.#onMessage);
|
|
1646
|
-
this.#endpoint.off("disconnect", this.#onDisconnect);
|
|
1647
|
-
this.#failAll("The cluster worker closed.");
|
|
1648
|
-
}
|
|
1649
|
-
#send(body) {
|
|
1650
|
-
try {
|
|
1651
|
-
this.#endpoint.send({ [CLUSTER_FRAME]: body });
|
|
1652
|
-
} catch (error) {
|
|
1653
|
-
this.#failAll(error instanceof Error ? error.message : String(error));
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
#failAll(reason) {
|
|
1657
|
-
for (const waiter of this.#waiting.values())
|
|
1658
|
-
waiter.reject(new Error(reason));
|
|
1659
|
-
this.#waiting.clear();
|
|
1660
|
-
}
|
|
1661
|
-
#handle(frame) {
|
|
1662
|
-
if (!frame || typeof frame !== "object" || !(CLUSTER_FRAME in frame))
|
|
1663
|
-
return;
|
|
1664
|
-
const parsed = parseParentFrame(frame[CLUSTER_FRAME]);
|
|
1665
|
-
if (!parsed)
|
|
1666
|
-
return;
|
|
1667
|
-
if (parsed.kind === "identify-grant" || parsed.kind === "identify-deny") {
|
|
1668
|
-
const waiter = this.#waiting.get(parsed.id);
|
|
1669
|
-
if (!waiter)
|
|
1670
|
-
return;
|
|
1671
|
-
this.#waiting.delete(parsed.id);
|
|
1672
|
-
if (parsed.kind === "identify-grant")
|
|
1673
|
-
waiter.resolve();
|
|
1674
|
-
else
|
|
1675
|
-
waiter.reject(new Error(parsed.error));
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
const handler = this.#handlers.get(parsed.type);
|
|
1679
|
-
const id = parsed.id;
|
|
1680
|
-
if (!handler) {
|
|
1681
|
-
this.#send({
|
|
1682
|
-
kind: "reply",
|
|
1683
|
-
id,
|
|
1684
|
-
error: `No handler for "${parsed.type}".`
|
|
1685
|
-
});
|
|
1686
|
-
return;
|
|
1687
|
-
}
|
|
1688
|
-
(async () => {
|
|
1689
|
-
try {
|
|
1690
|
-
this.#send({
|
|
1691
|
-
kind: "reply",
|
|
1692
|
-
id,
|
|
1693
|
-
result: await handler(parsed.data)
|
|
1694
|
-
});
|
|
1695
|
-
} catch (error) {
|
|
1696
|
-
this.#send({
|
|
1697
|
-
kind: "reply",
|
|
1698
|
-
id,
|
|
1699
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1700
|
-
});
|
|
1701
|
-
}
|
|
1702
|
-
})();
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
// packages/sharding/src/transport.ts
|
|
1706
|
-
class BroadcastChannelTransport {
|
|
1707
|
-
#channel;
|
|
1708
|
-
constructor(name) {
|
|
1709
|
-
this.#channel = new BroadcastChannel(name);
|
|
1710
|
-
}
|
|
1711
|
-
post(message) {
|
|
1712
|
-
this.#channel.postMessage(message);
|
|
1713
|
-
}
|
|
1714
|
-
onMessage(listener) {
|
|
1715
|
-
this.#channel.addEventListener("message", (event) => listener(event.data));
|
|
1716
|
-
}
|
|
1717
|
-
close() {
|
|
1718
|
-
this.#channel.close();
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
var BUS_FRAME = "__lunibeeBus";
|
|
1722
|
-
var PEER_DOWN_FRAME = "__lunibeePeerDown";
|
|
1723
|
-
var LOCAL = new WeakMap;
|
|
1724
|
-
|
|
1725
|
-
class IpcTransport {
|
|
1726
|
-
#endpoint;
|
|
1727
|
-
#listener;
|
|
1728
|
-
#message;
|
|
1729
|
-
#down;
|
|
1730
|
-
constructor(endpoint = process) {
|
|
1731
|
-
if (typeof endpoint.send !== "function")
|
|
1732
|
-
throw new Error("IpcTransport needs an IPC channel: run the shard in a process forked by ClusterManager.");
|
|
1733
|
-
this.#endpoint = endpoint;
|
|
1734
|
-
this.#listener = (frame) => {
|
|
1735
|
-
const record = frame;
|
|
1736
|
-
if (!record || typeof record !== "object")
|
|
1737
|
-
return;
|
|
1738
|
-
if (BUS_FRAME in record)
|
|
1739
|
-
this.#message?.(record[BUS_FRAME]);
|
|
1740
|
-
else if (PEER_DOWN_FRAME in record)
|
|
1741
|
-
this.#down?.(record[PEER_DOWN_FRAME]);
|
|
1742
|
-
};
|
|
1743
|
-
endpoint.on("message", this.#listener);
|
|
1744
|
-
let local = LOCAL.get(endpoint);
|
|
1745
|
-
if (!local)
|
|
1746
|
-
LOCAL.set(endpoint, local = new Set);
|
|
1747
|
-
local.add(this);
|
|
1748
|
-
}
|
|
1749
|
-
post(message) {
|
|
1750
|
-
this.#endpoint.send({ [BUS_FRAME]: message });
|
|
1751
|
-
for (const peer of LOCAL.get(this.#endpoint) ?? [])
|
|
1752
|
-
if (peer !== this) {
|
|
1753
|
-
const copy = structuredClone(message);
|
|
1754
|
-
queueMicrotask(() => peer.#message?.(copy));
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
onMessage(listener) {
|
|
1758
|
-
this.#message = listener;
|
|
1759
|
-
}
|
|
1760
|
-
onPeerDown(listener) {
|
|
1761
|
-
this.#down = listener;
|
|
1762
|
-
}
|
|
1763
|
-
close() {
|
|
1764
|
-
this.#endpoint.off("message", this.#listener);
|
|
1765
|
-
LOCAL.get(this.#endpoint)?.delete(this);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
function defaultTransport(name) {
|
|
1769
|
-
if (process.env.LUNIBEE_SHARD_BUS === "ipc") {
|
|
1770
|
-
if (typeof process.send === "function")
|
|
1771
|
-
return new IpcTransport;
|
|
1772
|
-
const thread = threadEndpoint();
|
|
1773
|
-
if (thread)
|
|
1774
|
-
return new IpcTransport(thread);
|
|
1775
|
-
}
|
|
1776
|
-
return new BroadcastChannelTransport(name);
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
// packages/sharding/src/bus.ts
|
|
1780
|
-
var REPLY_TYPE = "__lunibee:reply";
|
|
1781
|
-
var MAX_NAME_LENGTH = 256;
|
|
1782
|
-
function isShardMessage(message) {
|
|
1783
|
-
if (!message || typeof message !== "object")
|
|
1784
|
-
return false;
|
|
1785
|
-
const m = message;
|
|
1786
|
-
return typeof m.type === "string" && m.type.length > 0 && m.type.length <= MAX_NAME_LENGTH && typeof m.id === "string" && m.id.length <= MAX_NAME_LENGTH && Number.isInteger(m.source) && m.source >= -1 && (m.target === null || Number.isInteger(m.target) && m.target >= -1);
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
class ShardBus {
|
|
1790
|
-
#transport;
|
|
1791
|
-
#maxPending;
|
|
1792
|
-
#handlers = new Map;
|
|
1793
|
-
#responders = new Map;
|
|
1794
|
-
#errorHandlers = new Set;
|
|
1795
|
-
#shardId;
|
|
1796
|
-
#namespace;
|
|
1797
|
-
channelName;
|
|
1798
|
-
#counter = 0;
|
|
1799
|
-
#pending = new Map;
|
|
1800
|
-
#closed = false;
|
|
1801
|
-
constructor(shardId, channelName, options = {}) {
|
|
1802
|
-
if (!Number.isInteger(shardId) || shardId < 0)
|
|
1803
|
-
throw new RangeError("Shard ID must be a non-negative integer.");
|
|
1804
|
-
if (!channelName?.trim())
|
|
1805
|
-
throw new TypeError("An application-specific shard channel name is required.");
|
|
1806
|
-
this.#shardId = shardId;
|
|
1807
|
-
this.#namespace = channelName.trim();
|
|
1808
|
-
this.channelName = this.#namespace;
|
|
1809
|
-
this.#maxPending = options.maxPending ?? 1e4;
|
|
1810
|
-
this.#transport = options.transport ?? defaultTransport(this.channelName);
|
|
1811
|
-
this.#transport.onMessage((message) => this.#dispatch(message));
|
|
1812
|
-
this.#transport.onPeerDown?.((shardIds) => this.#peerDown(shardIds));
|
|
1813
|
-
}
|
|
1814
|
-
on(type, handler) {
|
|
1815
|
-
if (!type.trim() || typeof handler !== "function")
|
|
1816
|
-
throw new TypeError("Shard message type and handler are required.");
|
|
1817
|
-
let handlers = this.#handlers.get(type);
|
|
1818
|
-
if (!handlers)
|
|
1819
|
-
this.#handlers.set(type, handlers = new Set);
|
|
1820
|
-
handlers.add(handler);
|
|
1821
|
-
return this;
|
|
1822
|
-
}
|
|
1823
|
-
off(type, handler) {
|
|
1824
|
-
const handlers = this.#handlers.get(type);
|
|
1825
|
-
handlers?.delete(handler);
|
|
1826
|
-
const responder = this.#responders.get(type)?.get(handler);
|
|
1827
|
-
if (responder) {
|
|
1828
|
-
handlers?.delete(responder);
|
|
1829
|
-
this.#responders.get(type).delete(handler);
|
|
1830
|
-
}
|
|
1831
|
-
return this;
|
|
1832
|
-
}
|
|
1833
|
-
onError(handler) {
|
|
1834
|
-
if (typeof handler !== "function")
|
|
1835
|
-
throw new TypeError("Shard bus error handler is required.");
|
|
1836
|
-
this.#errorHandlers.add(handler);
|
|
1837
|
-
return this;
|
|
1838
|
-
}
|
|
1839
|
-
offError(handler) {
|
|
1840
|
-
this.#errorHandlers.delete(handler);
|
|
1841
|
-
return this;
|
|
1842
|
-
}
|
|
1843
|
-
send(target, type, data) {
|
|
1844
|
-
if (!Number.isInteger(target) || target < 0)
|
|
1845
|
-
throw new RangeError("Shard target must be a non-negative integer.");
|
|
1846
|
-
return this.#publish(target, type, data);
|
|
1847
|
-
}
|
|
1848
|
-
broadcast(type, data) {
|
|
1849
|
-
return this.#publish(null, type, data);
|
|
1850
|
-
}
|
|
1851
|
-
respond(type, handler) {
|
|
1852
|
-
const wrapper = async (message) => {
|
|
1853
|
-
if (!message.expectsReply)
|
|
1854
|
-
return;
|
|
1855
|
-
const reply = { requestId: message.id };
|
|
1856
|
-
try {
|
|
1857
|
-
reply.result = await handler(message.data, message);
|
|
1858
|
-
} catch (error) {
|
|
1859
|
-
reply.error = error instanceof Error ? error.message : String(error);
|
|
1860
|
-
}
|
|
1861
|
-
this.#publish(message.source, REPLY_TYPE, reply);
|
|
1862
|
-
};
|
|
1863
|
-
let byType = this.#responders.get(type);
|
|
1864
|
-
if (!byType)
|
|
1865
|
-
this.#responders.set(type, byType = new Map);
|
|
1866
|
-
byType.set(handler, wrapper);
|
|
1867
|
-
return this.on(type, wrapper);
|
|
1868
|
-
}
|
|
1869
|
-
async request(target, type, data, timeoutMs = 5000) {
|
|
1870
|
-
if (!Number.isInteger(target) || target < 0)
|
|
1871
|
-
throw new RangeError("Shard target must be a non-negative integer.");
|
|
1872
|
-
const [reply] = await this.#collect(target, type, data, timeoutMs, 1, true);
|
|
1873
|
-
if (!reply)
|
|
1874
|
-
throw new Error(`Shard ${target} did not reply to "${type}" within ${timeoutMs}ms.`);
|
|
1875
|
-
if (reply.error !== undefined)
|
|
1876
|
-
throw new Error(reply.error);
|
|
1877
|
-
return reply.result;
|
|
1878
|
-
}
|
|
1879
|
-
broadcastRequest(type, data, options = {}) {
|
|
1880
|
-
return this.#collect(null, type, data, options.timeoutMs ?? 5000, options.expected ?? Number.POSITIVE_INFINITY);
|
|
1881
|
-
}
|
|
1882
|
-
#collect(target, type, data, timeoutMs, expected, rejectOnPeerDown = false) {
|
|
1883
|
-
const replies = [];
|
|
1884
|
-
if (this.#closed)
|
|
1885
|
-
return Promise.reject(new Error("ShardBus is closed."));
|
|
1886
|
-
if (this.#pending.size >= this.#maxPending)
|
|
1887
|
-
return Promise.reject(new Error(`Too many shard requests waiting for a reply (${this.#maxPending}).`));
|
|
1888
|
-
return new Promise((resolve, reject) => {
|
|
1889
|
-
let requestId = "";
|
|
1890
|
-
const finish = () => {
|
|
1891
|
-
clearTimeout(timer);
|
|
1892
|
-
this.#pending.delete(requestId);
|
|
1893
|
-
resolve(replies);
|
|
1894
|
-
};
|
|
1895
|
-
const timer = setTimeout(finish, timeoutMs);
|
|
1896
|
-
requestId = this.#publish(target, type, data, true);
|
|
1897
|
-
const fail = (error) => {
|
|
1898
|
-
if (!rejectOnPeerDown)
|
|
1899
|
-
return;
|
|
1900
|
-
clearTimeout(timer);
|
|
1901
|
-
this.#pending.delete(requestId);
|
|
1902
|
-
reject(error);
|
|
1903
|
-
};
|
|
1904
|
-
this.#pending.set(requestId, {
|
|
1905
|
-
target,
|
|
1906
|
-
seen: new Set,
|
|
1907
|
-
reply: (reply) => {
|
|
1908
|
-
replies.push(reply);
|
|
1909
|
-
if (replies.length >= expected)
|
|
1910
|
-
finish();
|
|
1911
|
-
},
|
|
1912
|
-
fail,
|
|
1913
|
-
cancel: (error) => rejectOnPeerDown ? fail(error) : finish()
|
|
1914
|
-
});
|
|
1915
|
-
});
|
|
1916
|
-
}
|
|
1917
|
-
close() {
|
|
1918
|
-
if (this.#closed)
|
|
1919
|
-
return;
|
|
1920
|
-
this.#closed = true;
|
|
1921
|
-
for (const pending of [...this.#pending.values()])
|
|
1922
|
-
pending.cancel(new Error("ShardBus is closed."));
|
|
1923
|
-
this.#pending.clear();
|
|
1924
|
-
this.#transport.close();
|
|
1925
|
-
this.#handlers.clear();
|
|
1926
|
-
this.#errorHandlers.clear();
|
|
1927
|
-
}
|
|
1928
|
-
#publish(target, type, data, expectsReply = false) {
|
|
1929
|
-
if (!type.trim())
|
|
1930
|
-
throw new TypeError("Shard message type is required.");
|
|
1931
|
-
if (this.#closed)
|
|
1932
|
-
throw new Error("ShardBus is closed.");
|
|
1933
|
-
const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
|
|
1934
|
-
this.#transport.post({
|
|
1935
|
-
source: this.#shardId,
|
|
1936
|
-
target,
|
|
1937
|
-
type,
|
|
1938
|
-
data,
|
|
1939
|
-
id,
|
|
1940
|
-
...expectsReply ? { expectsReply } : {}
|
|
1941
|
-
});
|
|
1942
|
-
return id;
|
|
1943
|
-
}
|
|
1944
|
-
#dispatch(message) {
|
|
1945
|
-
if (this.#closed || !isShardMessage(message))
|
|
1946
|
-
return;
|
|
1947
|
-
if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
|
|
1948
|
-
return;
|
|
1949
|
-
if (message.type === REPLY_TYPE) {
|
|
1950
|
-
const reply = message.data;
|
|
1951
|
-
if (!reply || typeof reply !== "object" || typeof reply.requestId !== "string")
|
|
1952
|
-
return;
|
|
1953
|
-
const pending = this.#pending.get(reply.requestId);
|
|
1954
|
-
if (!pending || pending.seen.has(message.source) || pending.target !== null && pending.target !== message.source)
|
|
1955
|
-
return;
|
|
1956
|
-
pending.seen.add(message.source);
|
|
1957
|
-
pending.reply({
|
|
1958
|
-
shardId: message.source,
|
|
1959
|
-
...reply.error !== undefined ? { error: String(reply.error) } : { result: reply.result }
|
|
1960
|
-
});
|
|
1961
|
-
return;
|
|
1962
|
-
}
|
|
1963
|
-
for (const handler of this.#handlers.get(message.type) ?? []) {
|
|
1964
|
-
try {
|
|
1965
|
-
const result = handler(message);
|
|
1966
|
-
if (result && typeof result.then === "function")
|
|
1967
|
-
Promise.resolve(result).catch((error) => this.#reportError(error, message));
|
|
1968
|
-
} catch (error) {
|
|
1969
|
-
this.#reportError(error, message);
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
#peerDown(shardIds) {
|
|
1974
|
-
const down = new Set(shardIds);
|
|
1975
|
-
for (const [id, pending] of this.#pending)
|
|
1976
|
-
if (pending.target !== null && down.has(pending.target)) {
|
|
1977
|
-
pending.fail(new Error(`Shard ${pending.target} went away before it replied.`));
|
|
1978
|
-
this.#pending.delete(id);
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
#reportError(error, message) {
|
|
1982
|
-
if (this.#errorHandlers.size === 0) {
|
|
1983
|
-
process.emitWarning(error instanceof Error ? error : new Error(String(error)), { code: "LUNIBEE_SHARD_BUS_ERROR" });
|
|
1984
|
-
return;
|
|
1985
|
-
}
|
|
1986
|
-
for (const handler of this.#errorHandlers) {
|
|
1987
|
-
try {
|
|
1988
|
-
handler(error, message);
|
|
1989
|
-
} catch {}
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
// packages/sharding/src/supervisor.ts
|
|
1994
|
-
class ShardSupervisor {
|
|
1995
|
-
#options;
|
|
1996
|
-
#restarts = new Map;
|
|
1997
|
-
#random;
|
|
1998
|
-
constructor(options = {}, random = Math.random) {
|
|
1999
|
-
this.#options = {
|
|
2000
|
-
restartDelay: options.restartDelay ?? 5000,
|
|
2001
|
-
backoff: options.backoff ?? 1,
|
|
2002
|
-
maxDelay: options.maxDelay ?? 300000,
|
|
2003
|
-
jitter: options.jitter ?? 0,
|
|
2004
|
-
maxRestarts: options.maxRestarts ?? Number.POSITIVE_INFINITY,
|
|
2005
|
-
window: options.window ?? 60000
|
|
2006
|
-
};
|
|
2007
|
-
const { restartDelay, backoff, jitter, maxDelay, window, maxRestarts } = this.#options;
|
|
2008
|
-
if (!(restartDelay >= 0) || !(maxDelay >= 0) || !(window > 0))
|
|
2009
|
-
throw new RangeError("restartDelay and maxDelay cannot be negative, and window must be positive.");
|
|
2010
|
-
if (!(backoff >= 1))
|
|
2011
|
-
throw new RangeError("backoff must be at least 1.");
|
|
2012
|
-
if (!(jitter >= 0 && jitter <= 1))
|
|
2013
|
-
throw new RangeError("jitter must be between 0 and 1.");
|
|
2014
|
-
if (!(maxRestarts >= 0))
|
|
2015
|
-
throw new RangeError("maxRestarts cannot be negative.");
|
|
2016
|
-
this.#random = random;
|
|
2017
|
-
}
|
|
2018
|
-
next(id, now = Date.now()) {
|
|
2019
|
-
const { window, maxRestarts, restartDelay, backoff, maxDelay, jitter } = this.#options;
|
|
2020
|
-
const recent = (this.#restarts.get(id) ?? []).filter((at) => now - at < window);
|
|
2021
|
-
if (recent.length >= maxRestarts) {
|
|
2022
|
-
this.#restarts.delete(id);
|
|
2023
|
-
return null;
|
|
2024
|
-
}
|
|
2025
|
-
recent.push(now);
|
|
2026
|
-
this.#restarts.set(id, recent);
|
|
2027
|
-
const base = Math.min(maxDelay, restartDelay * backoff ** (recent.length - 1));
|
|
2028
|
-
return Math.min(maxDelay, Math.round(base * (1 + jitter * this.#random())));
|
|
2029
|
-
}
|
|
2030
|
-
reset(id) {
|
|
2031
|
-
if (id === undefined)
|
|
2032
|
-
this.#restarts.clear();
|
|
2033
|
-
else
|
|
2034
|
-
this.#restarts.delete(id);
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
// packages/sharding/src/cluster.ts
|
|
2038
|
-
import { fork } from "child_process";
|
|
2039
|
-
import { cpus } from "os";
|
|
2040
|
-
|
|
2041
|
-
// packages/sharding/src/identify.ts
|
|
2042
|
-
var IDENTIFY_INTERVAL = 5000;
|
|
2043
|
-
|
|
2044
|
-
class IdentifyCoordinator {
|
|
2045
|
-
maxConcurrency;
|
|
2046
|
-
#interval;
|
|
2047
|
-
#holdTimeout;
|
|
2048
|
-
#now;
|
|
2049
|
-
#schedule;
|
|
2050
|
-
#cancel;
|
|
2051
|
-
#onBudgetWait;
|
|
2052
|
-
#keys = new Map;
|
|
2053
|
-
#remaining;
|
|
2054
|
-
#total;
|
|
2055
|
-
#resetAt = Number.POSITIVE_INFINITY;
|
|
2056
|
-
#period = 24 * 60 * 60 * 1000;
|
|
2057
|
-
#timer;
|
|
2058
|
-
#timerAt = Number.POSITIVE_INFINITY;
|
|
2059
|
-
#closed = false;
|
|
2060
|
-
constructor(options = {}) {
|
|
2061
|
-
const concurrency = options.maxConcurrency ?? 1;
|
|
2062
|
-
if (!Number.isInteger(concurrency) || concurrency < 1)
|
|
2063
|
-
throw new RangeError("maxConcurrency must be a positive integer.");
|
|
2064
|
-
const interval = options.interval ?? IDENTIFY_INTERVAL;
|
|
2065
|
-
const hold = options.holdTimeout ?? 60000;
|
|
2066
|
-
if (!(interval >= 0) || !(hold > 0))
|
|
2067
|
-
throw new RangeError("interval cannot be negative and holdTimeout must be positive.");
|
|
2068
|
-
this.maxConcurrency = concurrency;
|
|
2069
|
-
this.#interval = interval;
|
|
2070
|
-
this.#holdTimeout = hold;
|
|
2071
|
-
this.#now = options.now ?? Date.now;
|
|
2072
|
-
this.#schedule = options.schedule ?? ((callback, ms) => {
|
|
2073
|
-
const timer = setTimeout(callback, ms);
|
|
2074
|
-
timer.unref?.();
|
|
2075
|
-
return timer;
|
|
2076
|
-
});
|
|
2077
|
-
this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle));
|
|
2078
|
-
this.#onBudgetWait = options.onBudgetWait;
|
|
2079
|
-
if (options.budget) {
|
|
2080
|
-
this.#total = options.budget.total;
|
|
2081
|
-
this.#remaining = options.budget.remaining;
|
|
2082
|
-
this.#resetAt = this.#now() + options.budget.resetAfter;
|
|
2083
|
-
this.#period = options.budget.period ?? this.#period;
|
|
2084
|
-
if (!(this.#period > 0))
|
|
2085
|
-
throw new RangeError("budget.period must be positive.");
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
get remaining() {
|
|
2089
|
-
return this.#remaining;
|
|
2090
|
-
}
|
|
2091
|
-
get waiting() {
|
|
2092
|
-
let count = 0;
|
|
2093
|
-
for (const key of this.#keys.values())
|
|
2094
|
-
count += key.queue.length;
|
|
2095
|
-
return count;
|
|
2096
|
-
}
|
|
2097
|
-
get holding() {
|
|
2098
|
-
let count = 0;
|
|
2099
|
-
for (const key of this.#keys.values())
|
|
2100
|
-
if (key.holder)
|
|
2101
|
-
count++;
|
|
2102
|
-
return count;
|
|
2103
|
-
}
|
|
2104
|
-
acquire(owner, shardId) {
|
|
2105
|
-
if (this.#closed)
|
|
2106
|
-
return Promise.reject(new Error("IDENTIFY coordinator is closed."));
|
|
2107
|
-
const index = shardId % this.maxConcurrency;
|
|
2108
|
-
let key = this.#keys.get(index);
|
|
2109
|
-
if (!key)
|
|
2110
|
-
this.#keys.set(index, key = { queue: [], nextAt: 0 });
|
|
2111
|
-
return new Promise((grant, fail) => {
|
|
2112
|
-
key.queue.push({ owner, shardId, grant, fail });
|
|
2113
|
-
this.pump();
|
|
2114
|
-
});
|
|
2115
|
-
}
|
|
2116
|
-
release(owner, shardId) {
|
|
2117
|
-
const key = this.#keys.get(shardId % this.maxConcurrency);
|
|
2118
|
-
const holder = key?.holder;
|
|
2119
|
-
if (!key || !holder || holder.owner !== owner || holder.shardId !== shardId)
|
|
2120
|
-
return;
|
|
2121
|
-
this.#free(key);
|
|
2122
|
-
this.pump();
|
|
2123
|
-
}
|
|
2124
|
-
drop(owner, reason = "The cluster went away.") {
|
|
2125
|
-
for (const key of this.#keys.values()) {
|
|
2126
|
-
const keep = [];
|
|
2127
|
-
for (const waiter of key.queue)
|
|
2128
|
-
if (waiter.owner === owner)
|
|
2129
|
-
waiter.fail(new Error(reason));
|
|
2130
|
-
else
|
|
2131
|
-
keep.push(waiter);
|
|
2132
|
-
key.queue = keep;
|
|
2133
|
-
if (key.holder?.owner === owner)
|
|
2134
|
-
this.#free(key);
|
|
2135
|
-
}
|
|
2136
|
-
this.pump();
|
|
2137
|
-
}
|
|
2138
|
-
close() {
|
|
2139
|
-
this.#closed = true;
|
|
2140
|
-
if (this.#timer !== undefined)
|
|
2141
|
-
this.#cancel(this.#timer);
|
|
2142
|
-
this.#timer = undefined;
|
|
2143
|
-
this.#timerAt = Number.POSITIVE_INFINITY;
|
|
2144
|
-
for (const key of this.#keys.values()) {
|
|
2145
|
-
if (key.holder)
|
|
2146
|
-
this.#cancel(key.holder.timer);
|
|
2147
|
-
key.holder = undefined;
|
|
2148
|
-
for (const waiter of key.queue)
|
|
2149
|
-
waiter.fail(new Error("IDENTIFY coordinator is closed."));
|
|
2150
|
-
key.queue = [];
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
pump() {
|
|
2154
|
-
if (this.#closed)
|
|
2155
|
-
return;
|
|
2156
|
-
const now = this.#now();
|
|
2157
|
-
if (this.#total !== undefined && now >= this.#resetAt) {
|
|
2158
|
-
this.#remaining = this.#total;
|
|
2159
|
-
while (now >= this.#resetAt)
|
|
2160
|
-
this.#resetAt += this.#period;
|
|
2161
|
-
}
|
|
2162
|
-
let next = Number.POSITIVE_INFINITY;
|
|
2163
|
-
for (const key of this.#keys.values()) {
|
|
2164
|
-
if (key.holder || key.queue.length === 0)
|
|
2165
|
-
continue;
|
|
2166
|
-
if (key.nextAt > now) {
|
|
2167
|
-
next = Math.min(next, key.nextAt);
|
|
2168
|
-
continue;
|
|
2169
|
-
}
|
|
2170
|
-
if (this.#remaining !== undefined && this.#remaining <= 0) {
|
|
2171
|
-
const waiter = key.queue[0];
|
|
2172
|
-
this.#onBudgetWait?.({
|
|
2173
|
-
shardId: waiter.shardId,
|
|
2174
|
-
waitMs: this.#resetAt - now
|
|
2175
|
-
});
|
|
2176
|
-
next = Math.min(next, this.#resetAt);
|
|
2177
|
-
continue;
|
|
2178
|
-
}
|
|
2179
|
-
const waiter = key.queue.shift();
|
|
2180
|
-
if (this.#remaining !== undefined)
|
|
2181
|
-
this.#remaining--;
|
|
2182
|
-
const timer = this.#schedule(() => {
|
|
2183
|
-
if (key.holder?.timer === timer) {
|
|
2184
|
-
this.#free(key);
|
|
2185
|
-
this.pump();
|
|
2186
|
-
}
|
|
2187
|
-
}, this.#holdTimeout);
|
|
2188
|
-
key.holder = {
|
|
2189
|
-
owner: waiter.owner,
|
|
2190
|
-
shardId: waiter.shardId,
|
|
2191
|
-
timer
|
|
2192
|
-
};
|
|
2193
|
-
waiter.grant();
|
|
2194
|
-
}
|
|
2195
|
-
this.#arm(next);
|
|
2196
|
-
}
|
|
2197
|
-
#free(key) {
|
|
2198
|
-
if (key.holder)
|
|
2199
|
-
this.#cancel(key.holder.timer);
|
|
2200
|
-
key.holder = undefined;
|
|
2201
|
-
key.nextAt = this.#now() + this.#interval;
|
|
2202
|
-
}
|
|
2203
|
-
#arm(at) {
|
|
2204
|
-
if (at === this.#timerAt && this.#timer !== undefined)
|
|
2205
|
-
return;
|
|
2206
|
-
if (this.#timer !== undefined)
|
|
2207
|
-
this.#cancel(this.#timer);
|
|
2208
|
-
this.#timer = undefined;
|
|
2209
|
-
this.#timerAt = Number.POSITIVE_INFINITY;
|
|
2210
|
-
if (at === Number.POSITIVE_INFINITY)
|
|
2211
|
-
return;
|
|
2212
|
-
this.#timerAt = at;
|
|
2213
|
-
this.#timer = this.#schedule(() => {
|
|
2214
|
-
this.#timer = undefined;
|
|
2215
|
-
this.#timerAt = Number.POSITIVE_INFINITY;
|
|
2216
|
-
this.pump();
|
|
2217
|
-
}, Math.max(0, at - this.#now()));
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
// packages/sharding/src/remote.ts
|
|
2222
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
2223
|
-
import { randomBytes } from "crypto";
|
|
2224
|
-
|
|
2225
|
-
// packages/sharding/src/remote-protocol.ts
|
|
2226
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
2227
|
-
var REMOTE_PROTOCOL_VERSION = 1;
|
|
2228
|
-
var MAX_REMOTE_FRAME_BYTES = 8 * 1024 * 1024;
|
|
2229
|
-
var RemoteClose = {
|
|
2230
|
-
BadRequest: 4400,
|
|
2231
|
-
AuthFailed: 4401,
|
|
2232
|
-
HelloTimeout: 4408,
|
|
2233
|
-
DuplicateHost: 4409,
|
|
2234
|
-
TooManyConnections: 4429,
|
|
2235
|
-
UnknownHost: 4410,
|
|
2236
|
-
VersionMismatch: 4426,
|
|
2237
|
-
HostLost: 4504,
|
|
2238
|
-
Shutdown: 4500
|
|
2239
|
-
};
|
|
2240
|
-
var SPAWN_ENV_KEYS = new Set([
|
|
2241
|
-
"SHARD_LIST",
|
|
2242
|
-
"SHARD_COUNT",
|
|
2243
|
-
"CLUSTER_ID",
|
|
2244
|
-
"CLUSTER_COUNT",
|
|
2245
|
-
"LUNIBEE_SHARD_BUS",
|
|
2246
|
-
"LUNIBEE_CLUSTER_WORKER",
|
|
2247
|
-
"LUNIBEE_IDENTIFY"
|
|
2248
|
-
]);
|
|
2249
|
-
var KILL_SIGNALS = new Set([
|
|
2250
|
-
"SIGTERM",
|
|
2251
|
-
"SIGKILL",
|
|
2252
|
-
"SIGINT"
|
|
2253
|
-
]);
|
|
2254
|
-
function isLoopbackHost(hostname) {
|
|
2255
|
-
return /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)$/i.test(hostname);
|
|
2256
|
-
}
|
|
2257
|
-
var HOST_ID = /^[A-Za-z0-9._-]{1,64}$/;
|
|
2258
|
-
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2259
|
-
var isInt = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
2260
|
-
function utf8Bytes(text) {
|
|
2261
|
-
return Buffer.byteLength(text, "utf8");
|
|
2262
|
-
}
|
|
2263
|
-
function parseJson(text, maxBytes) {
|
|
2264
|
-
if (typeof text !== "string")
|
|
2265
|
-
return null;
|
|
2266
|
-
if (text.length > maxBytes)
|
|
2267
|
-
return null;
|
|
2268
|
-
if (text.length * 3 > maxBytes && utf8Bytes(text) > maxBytes)
|
|
2269
|
-
return null;
|
|
2270
|
-
try {
|
|
2271
|
-
const value = JSON.parse(text);
|
|
2272
|
-
return isRecord2(value) ? value : null;
|
|
2273
|
-
} catch {
|
|
2274
|
-
return null;
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
function signChallenge(secret, nonce, hostId) {
|
|
2278
|
-
return createHmac("sha256", secret).update(`${nonce}:${hostId}`).digest("hex");
|
|
2279
|
-
}
|
|
2280
|
-
function verifyChallenge(secret, nonce, hostId, mac) {
|
|
2281
|
-
if (typeof mac !== "string")
|
|
2282
|
-
return false;
|
|
2283
|
-
const expected = Buffer.from(signChallenge(secret, nonce, hostId), "hex");
|
|
2284
|
-
const given = Buffer.from(mac, "hex");
|
|
2285
|
-
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
2286
|
-
}
|
|
2287
|
-
function parseToHost(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
|
|
2288
|
-
const v = parseJson(text, maxBytes);
|
|
2289
|
-
if (!v)
|
|
2290
|
-
return null;
|
|
2291
|
-
switch (v.t) {
|
|
2292
|
-
case "challenge":
|
|
2293
|
-
return typeof v.nonce === "string" && isInt(v.version) ? { t: "challenge", nonce: v.nonce, version: v.version } : null;
|
|
2294
|
-
case "welcome":
|
|
2295
|
-
return isInt(v.leaseMs) && isInt(v.pingMs) ? { t: "welcome", leaseMs: v.leaseMs, pingMs: v.pingMs } : null;
|
|
2296
|
-
case "ping":
|
|
2297
|
-
return isInt(v.n) ? { t: "ping", n: v.n } : null;
|
|
2298
|
-
case "spawn": {
|
|
2299
|
-
if (!isInt(v.cluster) || !isInt(v.epoch) || !isRecord2(v.env))
|
|
2300
|
-
return null;
|
|
2301
|
-
const env = {};
|
|
2302
|
-
for (const [key, value] of Object.entries(v.env)) {
|
|
2303
|
-
if (!SPAWN_ENV_KEYS.has(key) || typeof value !== "string")
|
|
2304
|
-
return null;
|
|
2305
|
-
env[key] = value;
|
|
2306
|
-
}
|
|
2307
|
-
return { t: "spawn", cluster: v.cluster, epoch: v.epoch, env };
|
|
2308
|
-
}
|
|
2309
|
-
case "frame":
|
|
2310
|
-
return isInt(v.cluster) && isInt(v.epoch) ? {
|
|
2311
|
-
t: "frame",
|
|
2312
|
-
cluster: v.cluster,
|
|
2313
|
-
epoch: v.epoch,
|
|
2314
|
-
frame: v.frame
|
|
2315
|
-
} : null;
|
|
2316
|
-
case "kill":
|
|
2317
|
-
return isInt(v.cluster) && isInt(v.epoch) && typeof v.signal === "string" && KILL_SIGNALS.has(v.signal) ? {
|
|
2318
|
-
t: "kill",
|
|
2319
|
-
cluster: v.cluster,
|
|
2320
|
-
epoch: v.epoch,
|
|
2321
|
-
signal: v.signal
|
|
2322
|
-
} : null;
|
|
2323
|
-
case "bye":
|
|
2324
|
-
return typeof v.reason === "string" ? { t: "bye", reason: v.reason } : null;
|
|
2325
|
-
default:
|
|
2326
|
-
return null;
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
function parseToCoordinator(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
|
|
2330
|
-
const v = parseJson(text, maxBytes);
|
|
2331
|
-
if (!v)
|
|
2332
|
-
return null;
|
|
2333
|
-
switch (v.t) {
|
|
2334
|
-
case "hello":
|
|
2335
|
-
return typeof v.id === "string" && typeof v.mac === "string" && isInt(v.version) ? { t: "hello", id: v.id, mac: v.mac, version: v.version } : null;
|
|
2336
|
-
case "pong":
|
|
2337
|
-
return isInt(v.n) ? { t: "pong", n: v.n } : null;
|
|
2338
|
-
case "spawned":
|
|
2339
|
-
return isInt(v.cluster) && isInt(v.epoch) && (v.pid === undefined || isInt(v.pid)) ? {
|
|
2340
|
-
t: "spawned",
|
|
2341
|
-
cluster: v.cluster,
|
|
2342
|
-
epoch: v.epoch,
|
|
2343
|
-
...v.pid !== undefined ? { pid: v.pid } : {}
|
|
2344
|
-
} : null;
|
|
2345
|
-
case "frame":
|
|
2346
|
-
return isInt(v.cluster) && isInt(v.epoch) ? {
|
|
2347
|
-
t: "frame",
|
|
2348
|
-
cluster: v.cluster,
|
|
2349
|
-
epoch: v.epoch,
|
|
2350
|
-
frame: v.frame
|
|
2351
|
-
} : null;
|
|
2352
|
-
case "exit":
|
|
2353
|
-
return isInt(v.cluster) && isInt(v.epoch) && (v.code === null || typeof v.code === "number") && (v.signal === null || typeof v.signal === "string") ? {
|
|
2354
|
-
t: "exit",
|
|
2355
|
-
cluster: v.cluster,
|
|
2356
|
-
epoch: v.epoch,
|
|
2357
|
-
code: v.code,
|
|
2358
|
-
signal: v.signal
|
|
2359
|
-
} : null;
|
|
2360
|
-
case "error":
|
|
2361
|
-
return isInt(v.cluster) && isInt(v.epoch) && typeof v.message === "string" ? {
|
|
2362
|
-
t: "error",
|
|
2363
|
-
cluster: v.cluster,
|
|
2364
|
-
epoch: v.epoch,
|
|
2365
|
-
message: v.message
|
|
2366
|
-
} : null;
|
|
2367
|
-
default:
|
|
2368
|
-
return null;
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
// packages/sharding/src/remote.ts
|
|
2373
|
-
class RemoteChild extends EventEmitter2 {
|
|
2374
|
-
host;
|
|
2375
|
-
cluster;
|
|
2376
|
-
epoch;
|
|
2377
|
-
pid;
|
|
2378
|
-
exitCode = null;
|
|
2379
|
-
signalCode = null;
|
|
2380
|
-
connected = true;
|
|
2381
|
-
#send;
|
|
2382
|
-
#kill;
|
|
2383
|
-
constructor(host, cluster, epoch, send, kill) {
|
|
2384
|
-
super();
|
|
2385
|
-
this.host = host;
|
|
2386
|
-
this.cluster = cluster;
|
|
2387
|
-
this.epoch = epoch;
|
|
2388
|
-
this.#send = send;
|
|
2389
|
-
this.#kill = kill;
|
|
2390
|
-
}
|
|
2391
|
-
send(frame) {
|
|
2392
|
-
return this.connected && this.exitCode === null && this.signalCode === null ? this.#send(frame) : false;
|
|
2393
|
-
}
|
|
2394
|
-
kill(signal = "SIGTERM") {
|
|
2395
|
-
if (this.exitCode !== null || this.signalCode !== null)
|
|
2396
|
-
return false;
|
|
2397
|
-
return this.#kill(signal);
|
|
2398
|
-
}
|
|
2399
|
-
spawned(pid) {
|
|
2400
|
-
this.pid = pid;
|
|
2401
|
-
}
|
|
2402
|
-
deliver(frame) {
|
|
2403
|
-
if (this.exitCode === null && this.signalCode === null)
|
|
2404
|
-
this.emit("message", frame);
|
|
2405
|
-
}
|
|
2406
|
-
finish(code, signal) {
|
|
2407
|
-
if (this.exitCode !== null || this.signalCode !== null)
|
|
2408
|
-
return;
|
|
2409
|
-
this.connected = false;
|
|
2410
|
-
this.exitCode = code ?? (signal === null ? 1 : null);
|
|
2411
|
-
this.signalCode = signal ?? null;
|
|
2412
|
-
this.emit("exit", code, signal);
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
class RemoteHosts {
|
|
2417
|
-
#options;
|
|
2418
|
-
#links = new Map;
|
|
2419
|
-
#droppedAt = new Map;
|
|
2420
|
-
#waiters = new Set;
|
|
2421
|
-
#hooks;
|
|
2422
|
-
#server;
|
|
2423
|
-
#watchdog;
|
|
2424
|
-
#epoch = 0;
|
|
2425
|
-
#ping = 0;
|
|
2426
|
-
#pending = new Set;
|
|
2427
|
-
#pendingByAddress = new Map;
|
|
2428
|
-
#rates = new Map;
|
|
2429
|
-
constructor(options, hooks) {
|
|
2430
|
-
if (typeof options.secret !== "string" || options.secret.length < 16)
|
|
2431
|
-
throw new RangeError("remote.secret must be a string of at least 16 characters.");
|
|
2432
|
-
if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)
|
|
2433
|
-
throw new RangeError("remote.port must be an integer from 0 to 65535.");
|
|
2434
|
-
if (options.tls && (typeof options.tls.cert !== "string" || typeof options.tls.key !== "string" || !options.tls.cert || !options.tls.key))
|
|
2435
|
-
throw new TypeError("remote.tls needs a non-empty cert and key.");
|
|
2436
|
-
if (!options.tls && !options.allowInsecure && !isLoopbackHost(options.hostname ?? "127.0.0.1"))
|
|
2437
|
-
throw new RangeError("remote listens on a network address without TLS: set remote.tls, bind 127.0.0.1 behind a TLS proxy, or set remote.allowInsecure for a network you fully trust.");
|
|
2438
|
-
const lease = options.lease ?? 15000;
|
|
2439
|
-
if (!Number.isFinite(lease) || lease < 30)
|
|
2440
|
-
throw new RangeError("remote.lease must be at least 30 milliseconds.");
|
|
2441
|
-
for (const id of options.hosts ?? [])
|
|
2442
|
-
if (!HOST_ID.test(id))
|
|
2443
|
-
throw new RangeError(`"${id}" is not a valid host ID.`);
|
|
2444
|
-
if (options.hosts && (options.hosts.length === 0 || new Set(options.hosts).size !== options.hosts.length))
|
|
2445
|
-
throw new RangeError("remote.hosts must be unique and not empty.");
|
|
2446
|
-
if (options.minHosts !== undefined && (!Number.isInteger(options.minHosts) || options.minHosts < 1))
|
|
2447
|
-
throw new RangeError("remote.minHosts must be a positive integer.");
|
|
2448
|
-
this.#options = { ...options, lease };
|
|
2449
|
-
this.#hooks = hooks;
|
|
2450
|
-
}
|
|
2451
|
-
get port() {
|
|
2452
|
-
return this.#server?.port;
|
|
2453
|
-
}
|
|
2454
|
-
connected() {
|
|
2455
|
-
return [...this.#links.keys()].sort();
|
|
2456
|
-
}
|
|
2457
|
-
isConnected(hostId) {
|
|
2458
|
-
return this.#links.has(hostId);
|
|
2459
|
-
}
|
|
2460
|
-
fencedAt(hostId) {
|
|
2461
|
-
const dropped = this.#droppedAt.get(hostId);
|
|
2462
|
-
return dropped === undefined || this.#links.has(hostId) ? 0 : dropped + this.#options.lease * 2;
|
|
2463
|
-
}
|
|
2464
|
-
start() {
|
|
2465
|
-
if (this.#server)
|
|
2466
|
-
return;
|
|
2467
|
-
const { port, hostname, tls, lease } = this.#options;
|
|
2468
|
-
this.#server = Bun.serve({
|
|
2469
|
-
port,
|
|
2470
|
-
hostname: hostname ?? "127.0.0.1",
|
|
2471
|
-
...tls ? { tls } : {},
|
|
2472
|
-
fetch: (request, server) => {
|
|
2473
|
-
const peer = server.requestIP(request)?.address;
|
|
2474
|
-
const address = this.#options.clientAddress?.(request, peer) ?? peer ?? "unknown";
|
|
2475
|
-
if (!this.#allowConnection(address))
|
|
2476
|
-
return new Response("Too many connections.", {
|
|
2477
|
-
status: 429
|
|
2478
|
-
});
|
|
2479
|
-
if (server.upgrade(request, {
|
|
2480
|
-
data: {
|
|
2481
|
-
nonce: randomBytes(16).toString("hex"),
|
|
2482
|
-
address
|
|
2483
|
-
}
|
|
2484
|
-
}))
|
|
2485
|
-
return;
|
|
2486
|
-
return new Response("Lunibee cluster coordinator", {
|
|
2487
|
-
status: 426
|
|
2488
|
-
});
|
|
2489
|
-
},
|
|
2490
|
-
websocket: {
|
|
2491
|
-
maxPayloadLength: MAX_REMOTE_FRAME_BYTES,
|
|
2492
|
-
idleTimeout: 0,
|
|
2493
|
-
open: (ws) => this.#open(ws),
|
|
2494
|
-
message: (ws, raw) => this.#message(ws, raw),
|
|
2495
|
-
close: (ws, _code, reason) => this.#closed(ws, reason)
|
|
2496
|
-
}
|
|
2497
|
-
});
|
|
2498
|
-
const pingMs = Math.max(10, Math.floor(lease / 3));
|
|
2499
|
-
this.#watchdog = setInterval(() => this.#tick(), pingMs);
|
|
2500
|
-
this.#watchdog.unref?.();
|
|
2501
|
-
}
|
|
2502
|
-
stop() {
|
|
2503
|
-
if (this.#watchdog)
|
|
2504
|
-
clearInterval(this.#watchdog);
|
|
2505
|
-
this.#watchdog = undefined;
|
|
2506
|
-
for (const link of [...this.#links.values()]) {
|
|
2507
|
-
this.#post(link, { t: "bye", reason: "The coordinator stopped." });
|
|
2508
|
-
this.#drop(link, "The coordinator stopped.", RemoteClose.Shutdown);
|
|
2509
|
-
}
|
|
2510
|
-
this.#server?.stop(true);
|
|
2511
|
-
this.#server = undefined;
|
|
2512
|
-
this.abortWaits();
|
|
2513
|
-
}
|
|
2514
|
-
waitFor(hosts, count, timeoutMs) {
|
|
2515
|
-
const done = () => hosts ? hosts.every((id) => this.#links.has(id)) : this.#links.size >= count;
|
|
2516
|
-
if (done())
|
|
2517
|
-
return Promise.resolve();
|
|
2518
|
-
return new Promise((resolve, reject) => {
|
|
2519
|
-
const waiter = {
|
|
2520
|
-
check: () => {
|
|
2521
|
-
if (!done())
|
|
2522
|
-
return;
|
|
2523
|
-
clearTimeout(timer);
|
|
2524
|
-
this.#waiters.delete(waiter);
|
|
2525
|
-
resolve();
|
|
2526
|
-
},
|
|
2527
|
-
abort: (error) => {
|
|
2528
|
-
clearTimeout(timer);
|
|
2529
|
-
this.#waiters.delete(waiter);
|
|
2530
|
-
reject(error);
|
|
2531
|
-
}
|
|
2532
|
-
};
|
|
2533
|
-
const timer = setTimeout(() => {
|
|
2534
|
-
const missing = hosts?.filter((id) => !this.#links.has(id));
|
|
2535
|
-
waiter.abort(new Error(hosts ? `Hosts did not join within ${timeoutMs}ms: [${missing}].` : `Only ${this.#links.size} of ${count} hosts joined within ${timeoutMs}ms.`));
|
|
2536
|
-
}, timeoutMs);
|
|
2537
|
-
this.#waiters.add(waiter);
|
|
2538
|
-
});
|
|
2539
|
-
}
|
|
2540
|
-
abortWaits() {
|
|
2541
|
-
for (const waiter of [...this.#waiters])
|
|
2542
|
-
waiter.abort(new Error("The coordinator stopped."));
|
|
2543
|
-
}
|
|
2544
|
-
status(declared = []) {
|
|
2545
|
-
const ids = new Set([...declared, ...this.#links.keys()]);
|
|
2546
|
-
return [...ids].sort().map((id) => {
|
|
2547
|
-
const link = this.#links.get(id);
|
|
2548
|
-
return {
|
|
2549
|
-
id,
|
|
2550
|
-
connected: link !== undefined,
|
|
2551
|
-
lastSeen: link?.lastSeen ?? 0,
|
|
2552
|
-
clusters: link ? [...link.children.keys()].sort((a, b) => a - b) : []
|
|
2553
|
-
};
|
|
2554
|
-
});
|
|
2555
|
-
}
|
|
2556
|
-
createChild(hostId, cluster, env) {
|
|
2557
|
-
const epoch = ++this.#epoch;
|
|
2558
|
-
const link = this.#links.get(hostId);
|
|
2559
|
-
const child = new RemoteChild(hostId, cluster, epoch, (frame) => link !== undefined && this.#post(link, { t: "frame", cluster, epoch, frame }), (signal) => link !== undefined && this.#post(link, { t: "kill", cluster, epoch, signal }));
|
|
2560
|
-
if (!link) {
|
|
2561
|
-
queueMicrotask(() => child.finish(null, "SIGHUP"));
|
|
2562
|
-
return child;
|
|
2563
|
-
}
|
|
2564
|
-
link.children.get(cluster)?.finish(null, "SIGKILL");
|
|
2565
|
-
link.children.set(cluster, child);
|
|
2566
|
-
if (!this.#post(link, { t: "spawn", cluster, epoch, env }))
|
|
2567
|
-
queueMicrotask(() => child.finish(null, "SIGHUP"));
|
|
2568
|
-
return child;
|
|
2569
|
-
}
|
|
2570
|
-
#post(link, message) {
|
|
2571
|
-
try {
|
|
2572
|
-
return link.ws.send(JSON.stringify(message)) > 0;
|
|
2573
|
-
} catch {
|
|
2574
|
-
return false;
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
#allowConnection(address) {
|
|
2578
|
-
const rate = this.#options.connectionsPerSecond ?? 10;
|
|
2579
|
-
const now = Date.now();
|
|
2580
|
-
let bucket = this.#rates.get(address);
|
|
2581
|
-
if (!bucket) {
|
|
2582
|
-
bucket = { tokens: rate, at: now };
|
|
2583
|
-
this.#rates.set(address, bucket);
|
|
2584
|
-
if (this.#rates.size > 1e4)
|
|
2585
|
-
this.#rates.delete(this.#rates.keys().next().value);
|
|
2586
|
-
}
|
|
2587
|
-
bucket.tokens = Math.min(rate, bucket.tokens + (now - bucket.at) / 1000 * rate);
|
|
2588
|
-
bucket.at = now;
|
|
2589
|
-
if (bucket.tokens < 1)
|
|
2590
|
-
return false;
|
|
2591
|
-
bucket.tokens--;
|
|
2592
|
-
return true;
|
|
2593
|
-
}
|
|
2594
|
-
#evict(victim) {
|
|
2595
|
-
this.#uncount(victim);
|
|
2596
|
-
clearTimeout(victim.data.helloTimer);
|
|
2597
|
-
victim.close(RemoteClose.TooManyConnections, "Too many connections.");
|
|
2598
|
-
}
|
|
2599
|
-
#open(ws) {
|
|
2600
|
-
const address = ws.data.address;
|
|
2601
|
-
if ((this.#pendingByAddress.get(address) ?? 0) >= (this.#options.maxUnauthenticatedPerAddress ?? 4)) {
|
|
2602
|
-
for (const other of this.#pending)
|
|
2603
|
-
if (other.data.address === address) {
|
|
2604
|
-
this.#evict(other);
|
|
2605
|
-
break;
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
if (this.#pending.size >= (this.#options.maxUnauthenticated ?? 16)) {
|
|
2609
|
-
const oldest = this.#pending.values().next().value;
|
|
2610
|
-
if (oldest)
|
|
2611
|
-
this.#evict(oldest);
|
|
2612
|
-
}
|
|
2613
|
-
this.#pending.add(ws);
|
|
2614
|
-
this.#pendingByAddress.set(address, (this.#pendingByAddress.get(address) ?? 0) + 1);
|
|
2615
|
-
ws.data.counted = true;
|
|
2616
|
-
ws.data.helloTimer = setTimeout(() => ws.close(RemoteClose.HelloTimeout, "No hello."), this.#options.helloTimeout ?? 3000);
|
|
2617
|
-
ws.send(JSON.stringify({
|
|
2618
|
-
t: "challenge",
|
|
2619
|
-
nonce: ws.data.nonce,
|
|
2620
|
-
version: REMOTE_PROTOCOL_VERSION
|
|
2621
|
-
}));
|
|
2622
|
-
}
|
|
2623
|
-
#message(ws, raw) {
|
|
2624
|
-
if (typeof raw !== "string") {
|
|
2625
|
-
ws.close(RemoteClose.BadRequest, "Text frames only.");
|
|
2626
|
-
return;
|
|
2627
|
-
}
|
|
2628
|
-
const message = parseToCoordinator(raw);
|
|
2629
|
-
if (!message) {
|
|
2630
|
-
ws.close(RemoteClose.BadRequest, "Malformed frame.");
|
|
2631
|
-
return;
|
|
2632
|
-
}
|
|
2633
|
-
if (ws.data.id === undefined) {
|
|
2634
|
-
this.#hello(ws, message);
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
const link = this.#links.get(ws.data.id);
|
|
2638
|
-
if (!link || link.ws !== ws)
|
|
2639
|
-
return;
|
|
2640
|
-
link.lastSeen = Date.now();
|
|
2641
|
-
if (message.t === "pong" || message.t === "hello")
|
|
2642
|
-
return;
|
|
2643
|
-
const child = link.children.get(message.cluster);
|
|
2644
|
-
if (!child || child.epoch !== message.epoch)
|
|
2645
|
-
return;
|
|
2646
|
-
switch (message.t) {
|
|
2647
|
-
case "spawned":
|
|
2648
|
-
child.spawned(message.pid);
|
|
2649
|
-
break;
|
|
2650
|
-
case "frame":
|
|
2651
|
-
child.deliver(message.frame);
|
|
2652
|
-
break;
|
|
2653
|
-
case "exit":
|
|
2654
|
-
link.children.delete(message.cluster);
|
|
2655
|
-
child.finish(message.code, message.signal);
|
|
2656
|
-
break;
|
|
2657
|
-
case "error":
|
|
2658
|
-
this.#hooks.error(new Error(`Host ${link.id} could not run cluster ${message.cluster}: ${message.message}`));
|
|
2659
|
-
link.children.delete(message.cluster);
|
|
2660
|
-
child.finish(1, null);
|
|
2661
|
-
break;
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
#hello(ws, message) {
|
|
2665
|
-
if (message?.t !== "hello") {
|
|
2666
|
-
ws.close(RemoteClose.BadRequest, "Expected hello.");
|
|
2667
|
-
return;
|
|
2668
|
-
}
|
|
2669
|
-
const { id } = message;
|
|
2670
|
-
if (message.version !== REMOTE_PROTOCOL_VERSION) {
|
|
2671
|
-
ws.close(RemoteClose.VersionMismatch, "Protocol version mismatch.");
|
|
2672
|
-
return;
|
|
2673
|
-
}
|
|
2674
|
-
if (!HOST_ID.test(id) || !verifyChallenge(this.#options.secret, ws.data.nonce, id, message.mac)) {
|
|
2675
|
-
ws.close(RemoteClose.AuthFailed, "Authentication failed.");
|
|
2676
|
-
return;
|
|
2677
|
-
}
|
|
2678
|
-
if (this.#options.hosts && !this.#options.hosts.includes(id)) {
|
|
2679
|
-
ws.close(RemoteClose.UnknownHost, "Host is not part of this deployment.");
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
if (this.#links.has(id)) {
|
|
2683
|
-
ws.close(RemoteClose.DuplicateHost, "A host with this ID is connected.");
|
|
2684
|
-
return;
|
|
2685
|
-
}
|
|
2686
|
-
clearTimeout(ws.data.helloTimer);
|
|
2687
|
-
this.#uncount(ws);
|
|
2688
|
-
ws.data.id = id;
|
|
2689
|
-
const link = {
|
|
2690
|
-
id,
|
|
2691
|
-
ws,
|
|
2692
|
-
lastSeen: Date.now(),
|
|
2693
|
-
children: new Map
|
|
2694
|
-
};
|
|
2695
|
-
this.#links.set(id, link);
|
|
2696
|
-
this.#droppedAt.delete(id);
|
|
2697
|
-
const lease = this.#options.lease;
|
|
2698
|
-
this.#post(link, {
|
|
2699
|
-
t: "welcome",
|
|
2700
|
-
leaseMs: lease,
|
|
2701
|
-
pingMs: Math.max(10, Math.floor(lease / 3))
|
|
2702
|
-
});
|
|
2703
|
-
this.#hooks.join(id);
|
|
2704
|
-
for (const waiter of [...this.#waiters])
|
|
2705
|
-
waiter.check();
|
|
2706
|
-
}
|
|
2707
|
-
#uncount(ws) {
|
|
2708
|
-
if (!ws.data.counted)
|
|
2709
|
-
return;
|
|
2710
|
-
ws.data.counted = false;
|
|
2711
|
-
this.#pending.delete(ws);
|
|
2712
|
-
const left = (this.#pendingByAddress.get(ws.data.address) ?? 1) - 1;
|
|
2713
|
-
if (left > 0)
|
|
2714
|
-
this.#pendingByAddress.set(ws.data.address, left);
|
|
2715
|
-
else
|
|
2716
|
-
this.#pendingByAddress.delete(ws.data.address);
|
|
2717
|
-
}
|
|
2718
|
-
#closed(ws, reason) {
|
|
2719
|
-
clearTimeout(ws.data.helloTimer);
|
|
2720
|
-
this.#uncount(ws);
|
|
2721
|
-
const link = ws.data.id ? this.#links.get(ws.data.id) : undefined;
|
|
2722
|
-
if (link && link.ws === ws)
|
|
2723
|
-
this.#drop(link, reason || "The connection closed.");
|
|
2724
|
-
}
|
|
2725
|
-
#drop(link, reason, code) {
|
|
2726
|
-
if (this.#links.get(link.id) !== link)
|
|
2727
|
-
return;
|
|
2728
|
-
this.#links.delete(link.id);
|
|
2729
|
-
this.#droppedAt.set(link.id, Date.now());
|
|
2730
|
-
if (this.#droppedAt.size > 1000)
|
|
2731
|
-
this.#droppedAt.delete(this.#droppedAt.keys().next().value);
|
|
2732
|
-
if (code !== undefined)
|
|
2733
|
-
try {
|
|
2734
|
-
link.ws.close(code, reason.slice(0, 100));
|
|
2735
|
-
} catch {}
|
|
2736
|
-
for (const child of [...link.children.values()])
|
|
2737
|
-
child.finish(null, "SIGHUP");
|
|
2738
|
-
link.children.clear();
|
|
2739
|
-
this.#hooks.leave(link.id, reason);
|
|
2740
|
-
}
|
|
2741
|
-
#tick() {
|
|
2742
|
-
const now = Date.now();
|
|
2743
|
-
for (const link of [...this.#links.values()]) {
|
|
2744
|
-
if (now - link.lastSeen > this.#options.lease * 2) {
|
|
2745
|
-
this.#drop(link, "The host stopped answering.", RemoteClose.HostLost);
|
|
2746
|
-
continue;
|
|
2747
|
-
}
|
|
2748
|
-
this.#post(link, { t: "ping", n: ++this.#ping });
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
// packages/sharding/src/requests.ts
|
|
2754
|
-
class RequestTracker {
|
|
2755
|
-
#pending = new Map;
|
|
2756
|
-
#maxPending;
|
|
2757
|
-
#prefix;
|
|
2758
|
-
#counter = 0;
|
|
2759
|
-
constructor(prefix = "parent", maxPending = 1e4) {
|
|
2760
|
-
if (!Number.isInteger(maxPending) || maxPending < 1)
|
|
2761
|
-
throw new RangeError("maxPending must be a positive integer.");
|
|
2762
|
-
this.#prefix = prefix;
|
|
2763
|
-
this.#maxPending = maxPending;
|
|
2764
|
-
}
|
|
2765
|
-
get size() {
|
|
2766
|
-
return this.#pending.size;
|
|
2767
|
-
}
|
|
2768
|
-
begin(options) {
|
|
2769
|
-
if (this.#pending.size >= this.#maxPending)
|
|
2770
|
-
throw new Error(`Too many requests waiting for a reply (${this.#maxPending}).`);
|
|
2771
|
-
const id = `${this.#prefix}:${++this.#counter}`;
|
|
2772
|
-
const done = new Promise((resolve) => {
|
|
2773
|
-
const finish = () => {
|
|
2774
|
-
const entry = this.#pending.get(id);
|
|
2775
|
-
if (!entry)
|
|
2776
|
-
return;
|
|
2777
|
-
this.#pending.delete(id);
|
|
2778
|
-
clearTimeout(entry.timer);
|
|
2779
|
-
entry.cleanup?.();
|
|
2780
|
-
resolve({
|
|
2781
|
-
replies: entry.replies,
|
|
2782
|
-
missing: [...entry.keys.keys()].filter((key) => !entry.answered.has(key))
|
|
2783
|
-
});
|
|
2784
|
-
};
|
|
2785
|
-
const entry = {
|
|
2786
|
-
keys: new Map(options.keys),
|
|
2787
|
-
replies: [],
|
|
2788
|
-
answered: new Set,
|
|
2789
|
-
finish,
|
|
2790
|
-
timer: setTimeout(finish, options.timeoutMs)
|
|
2791
|
-
};
|
|
2792
|
-
if (options.signal) {
|
|
2793
|
-
const signal = options.signal;
|
|
2794
|
-
if (signal.aborted)
|
|
2795
|
-
queueMicrotask(finish);
|
|
2796
|
-
else {
|
|
2797
|
-
signal.addEventListener("abort", finish, { once: true });
|
|
2798
|
-
entry.cleanup = () => signal.removeEventListener("abort", finish);
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
this.#pending.set(id, entry);
|
|
2802
|
-
if (entry.keys.size === 0)
|
|
2803
|
-
queueMicrotask(finish);
|
|
2804
|
-
});
|
|
2805
|
-
return { id, done };
|
|
2806
|
-
}
|
|
2807
|
-
answer(id, key, from, value) {
|
|
2808
|
-
const entry = this.#pending.get(id);
|
|
2809
|
-
if (!entry || entry.keys.get(key) !== from)
|
|
2810
|
-
return false;
|
|
2811
|
-
if (entry.answered.has(key))
|
|
2812
|
-
return false;
|
|
2813
|
-
entry.answered.add(key);
|
|
2814
|
-
entry.replies.push({ key, ...value });
|
|
2815
|
-
if (entry.replies.length >= entry.keys.size)
|
|
2816
|
-
entry.finish();
|
|
2817
|
-
return true;
|
|
2818
|
-
}
|
|
2819
|
-
failOwner(owner, error) {
|
|
2820
|
-
for (const entry of [...this.#pending.values()]) {
|
|
2821
|
-
for (const [key, keyOwner] of entry.keys)
|
|
2822
|
-
if (keyOwner === owner && !entry.answered.has(key)) {
|
|
2823
|
-
entry.answered.add(key);
|
|
2824
|
-
entry.replies.push({ key, error });
|
|
2825
|
-
}
|
|
2826
|
-
if (entry.replies.length >= entry.keys.size)
|
|
2827
|
-
entry.finish();
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
close() {
|
|
2831
|
-
for (const entry of [...this.#pending.values()])
|
|
2832
|
-
entry.finish();
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
|
|
2836
|
-
// packages/sharding/src/cluster.ts
|
|
2837
|
-
var DEFAULT_MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
2838
|
-
function payloadSize(value) {
|
|
2839
|
-
if (value === undefined || value === null)
|
|
2840
|
-
return 0;
|
|
2841
|
-
if (typeof value === "string")
|
|
2842
|
-
return Buffer.byteLength(value, "utf8");
|
|
2843
|
-
if (typeof value !== "object")
|
|
2844
|
-
return String(value).length;
|
|
2845
|
-
try {
|
|
2846
|
-
const text = JSON.stringify(value);
|
|
2847
|
-
return text === undefined ? 0 : Buffer.byteLength(text, "utf8");
|
|
2848
|
-
} catch {
|
|
2849
|
-
return Number.POSITIVE_INFINITY;
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
var isLive = (state) => state === "starting" || state === "ready";
|
|
2853
|
-
|
|
2854
|
-
class ClusterManager {
|
|
2855
|
-
clusters = new Map;
|
|
2856
|
-
shardCount = 0;
|
|
2857
|
-
#options;
|
|
2858
|
-
#auto;
|
|
2859
|
-
#autoScaleTimer;
|
|
2860
|
-
#infos = new Map;
|
|
2861
|
-
#owner = new Map;
|
|
2862
|
-
#startPromise;
|
|
2863
|
-
#stopPromise;
|
|
2864
|
-
#wasStarted = false;
|
|
2865
|
-
#stopping = false;
|
|
2866
|
-
#generation = 0;
|
|
2867
|
-
#leading = false;
|
|
2868
|
-
#renewTimer;
|
|
2869
|
-
#lastRenew = 0;
|
|
2870
|
-
#renewing = false;
|
|
2871
|
-
#restartTimers = new Set;
|
|
2872
|
-
#fencing = new Map;
|
|
2873
|
-
#startTimers = new Map;
|
|
2874
|
-
#pauses = new Set;
|
|
2875
|
-
#readyWaiters = new Set;
|
|
2876
|
-
#supervisor;
|
|
2877
|
-
#tracker;
|
|
2878
|
-
#coordinator;
|
|
2879
|
-
#allReady = false;
|
|
2880
|
-
#clusterCount = 0;
|
|
2881
|
-
#identifyWaiting = new Map;
|
|
2882
|
-
#remote;
|
|
2883
|
-
#hostOf = new Map;
|
|
2884
|
-
#lastWait = "";
|
|
2885
|
-
#listeners = new Map;
|
|
2886
|
-
constructor(options) {
|
|
2887
|
-
if (!options.token?.trim())
|
|
2888
|
-
throw new TypeError("A Discord bot token is required.");
|
|
2889
|
-
if (!options.script?.trim())
|
|
2890
|
-
throw new TypeError("A script path is required for clustering.");
|
|
2891
|
-
if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
|
|
2892
|
-
throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
|
|
2893
|
-
}
|
|
2894
|
-
if (options.clusterCount !== undefined && options.shardsPerCluster !== undefined)
|
|
2895
|
-
throw new RangeError("Set either clusterCount or shardsPerCluster, not both.");
|
|
2896
|
-
for (const [name, value] of [
|
|
2897
|
-
["clusterCount", options.clusterCount],
|
|
2898
|
-
["shardsPerCluster", options.shardsPerCluster],
|
|
2899
|
-
["maxConcurrency", options.maxConcurrency]
|
|
2900
|
-
])
|
|
2901
|
-
if (value !== undefined && (!Number.isInteger(value) || value < 1))
|
|
2902
|
-
throw new RangeError(`${name} must be a positive integer.`);
|
|
2903
|
-
if (options.maxMessageBytes !== undefined && (!Number.isInteger(options.maxMessageBytes) || options.maxMessageBytes < 1))
|
|
2904
|
-
throw new RangeError("maxMessageBytes must be a positive integer.");
|
|
2905
|
-
for (const [name, value] of [
|
|
2906
|
-
["spawnInterval", options.spawnInterval],
|
|
2907
|
-
["startTimeout", options.startTimeout],
|
|
2908
|
-
["requestTimeout", options.requestTimeout]
|
|
2909
|
-
])
|
|
2910
|
-
if (value !== undefined && !(value >= 0))
|
|
2911
|
-
throw new RangeError(`${name} cannot be negative.`);
|
|
2912
|
-
if (options.guildsPerShard !== undefined) {
|
|
2913
|
-
assertGuildsPerShard(options.guildsPerShard);
|
|
2914
|
-
if (typeof options.shardCount === "number")
|
|
2915
|
-
throw new RangeError('guildsPerShard needs shardCount: "auto".');
|
|
2916
|
-
}
|
|
2917
|
-
if (options.shardIds && typeof options.shardCount === "number")
|
|
2918
|
-
validateShardIds(options.shardIds, options.shardCount);
|
|
2919
|
-
this.#auto = options.shardCount === "auto";
|
|
2920
|
-
const ttl = options.leader?.ttl;
|
|
2921
|
-
if (options.leader && (typeof options.leader.lock?.acquire !== "function" || typeof options.leader.lock.release !== "function"))
|
|
2922
|
-
throw new TypeError("leader.lock must have acquire() and release().");
|
|
2923
|
-
if (ttl !== undefined && (!Number.isInteger(ttl) || ttl < 100))
|
|
2924
|
-
throw new RangeError("leader.ttl must be an integer of at least 100.");
|
|
2925
|
-
this.#options = { ...options };
|
|
2926
|
-
this.#supervisor = new ShardSupervisor({
|
|
2927
|
-
restartDelay: options.restartDelay,
|
|
2928
|
-
...options.supervisor
|
|
2929
|
-
});
|
|
2930
|
-
this.#tracker = new RequestTracker("parent", options.maxPendingRequests);
|
|
2931
|
-
if (options.remote)
|
|
2932
|
-
this.#remote = new RemoteHosts(options.remote, {
|
|
2933
|
-
join: (id) => this.#hostJoined(id),
|
|
2934
|
-
leave: (id, reason) => this.#emit("hostLeave", id, reason),
|
|
2935
|
-
error: (error) => this.#emit("error", error)
|
|
2936
|
-
});
|
|
2937
|
-
}
|
|
2938
|
-
on(event, listener) {
|
|
2939
|
-
let set = this.#listeners.get(event);
|
|
2940
|
-
if (!set)
|
|
2941
|
-
this.#listeners.set(event, set = new Set);
|
|
2942
|
-
set.add(listener);
|
|
2943
|
-
return this;
|
|
2944
|
-
}
|
|
2945
|
-
off(event, listener) {
|
|
2946
|
-
this.#listeners.get(event)?.delete(listener);
|
|
2947
|
-
return this;
|
|
2948
|
-
}
|
|
2949
|
-
#emit(event, ...args) {
|
|
2950
|
-
for (const listener of this.#listeners.get(event) ?? [])
|
|
2951
|
-
try {
|
|
2952
|
-
listener(...args);
|
|
2953
|
-
} catch {}
|
|
2954
|
-
}
|
|
2955
|
-
async fetchRecommendedShardCount() {
|
|
2956
|
-
return resolveShardCount(this.#options.token, await fetchGatewayBot(this.#options.token), this.#options);
|
|
2957
|
-
}
|
|
2958
|
-
clusterForShard(shardId) {
|
|
2959
|
-
const id = this.#owner.get(shardId);
|
|
2960
|
-
return id === undefined ? undefined : this.#infos.get(id);
|
|
2961
|
-
}
|
|
2962
|
-
assignments() {
|
|
2963
|
-
return new Map(this.#owner);
|
|
2964
|
-
}
|
|
2965
|
-
spawn() {
|
|
2966
|
-
if (isClusterWorker())
|
|
2967
|
-
return Promise.reject(new Error("A cluster worker cannot start a ClusterManager: it would fork the whole fleet again."));
|
|
2968
|
-
if (this.#stopPromise)
|
|
2969
|
-
return this.#stopPromise.then(() => this.spawn());
|
|
2970
|
-
if (!this.#startPromise) {
|
|
2971
|
-
const attempt = this.#start();
|
|
2972
|
-
this.#startPromise = attempt;
|
|
2973
|
-
attempt.catch(() => {
|
|
2974
|
-
if (this.#startPromise === attempt)
|
|
2975
|
-
this.#startPromise = undefined;
|
|
2976
|
-
});
|
|
2977
|
-
}
|
|
2978
|
-
return this.#startPromise;
|
|
2979
|
-
}
|
|
2980
|
-
async#resolveGateway() {
|
|
2981
|
-
const { shardCount, maxConcurrency, fetchLimits } = this.#options;
|
|
2982
|
-
let info;
|
|
2983
|
-
let count;
|
|
2984
|
-
if (shardCount === "auto" || shardCount === undefined) {
|
|
2985
|
-
info = await fetchGatewayBot(this.#options.token);
|
|
2986
|
-
count = await resolveShardCount(this.#options.token, info, this.#options);
|
|
2987
|
-
} else if (fetchLimits)
|
|
2988
|
-
try {
|
|
2989
|
-
info = await fetchGatewayBot(this.#options.token);
|
|
2990
|
-
} catch (error) {
|
|
2991
|
-
this.#emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
2992
|
-
}
|
|
2993
|
-
const limit = info?.sessionStartLimit;
|
|
2994
|
-
return {
|
|
2995
|
-
count: count ?? shardCount,
|
|
2996
|
-
maxConcurrency: Math.min(maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Number.POSITIVE_INFINITY),
|
|
2997
|
-
...limit ? {
|
|
2998
|
-
budget: {
|
|
2999
|
-
total: limit.total,
|
|
3000
|
-
remaining: limit.remaining,
|
|
3001
|
-
resetAfter: limit.resetAfter
|
|
3002
|
-
}
|
|
3003
|
-
} : {}
|
|
3004
|
-
};
|
|
3005
|
-
}
|
|
3006
|
-
async#start() {
|
|
3007
|
-
const generation = this.#generation;
|
|
3008
|
-
const stale = () => this.#stopping || generation !== this.#generation;
|
|
3009
|
-
if (this.#options.leader && !this.#leading) {
|
|
3010
|
-
await this.#lead();
|
|
3011
|
-
if (stale())
|
|
3012
|
-
return;
|
|
3013
|
-
}
|
|
3014
|
-
const { count, maxConcurrency, budget } = await this.#resolveGateway();
|
|
3015
|
-
if (stale())
|
|
3016
|
-
return;
|
|
3017
|
-
const ids = this.#options.shardIds ?? Array.from({ length: count }, (_, i) => i);
|
|
3018
|
-
validateShardIds(ids, count);
|
|
3019
|
-
const clusterCount = this.#options.clusterCount ?? (this.#options.shardsPerCluster ? Math.ceil(ids.length / this.#options.shardsPerCluster) : cpus().length);
|
|
3020
|
-
const chunks = allocateShards(ids, clusterCount);
|
|
3021
|
-
assertCompleteAllocation(chunks, ids);
|
|
3022
|
-
const placement = await this.#placeHosts().catch((error) => {
|
|
3023
|
-
if (stale())
|
|
3024
|
-
return;
|
|
3025
|
-
throw error;
|
|
3026
|
-
});
|
|
3027
|
-
if (!placement || stale())
|
|
3028
|
-
return;
|
|
3029
|
-
this.shardCount = count;
|
|
3030
|
-
this.#wasStarted = true;
|
|
3031
|
-
this.#owner.clear();
|
|
3032
|
-
this.#infos.clear();
|
|
3033
|
-
const identify = this.#options.identify;
|
|
3034
|
-
this.#coordinator = identify?.coordinate === false ? undefined : new IdentifyCoordinator({
|
|
3035
|
-
maxConcurrency,
|
|
3036
|
-
interval: identify?.interval,
|
|
3037
|
-
holdTimeout: identify?.holdTimeout,
|
|
3038
|
-
budget,
|
|
3039
|
-
onBudgetWait: (wait) => {
|
|
3040
|
-
const key = `${wait.shardId}:${Math.round(wait.waitMs / 1000)}`;
|
|
3041
|
-
if (key === this.#lastWait)
|
|
3042
|
-
return;
|
|
3043
|
-
this.#lastWait = key;
|
|
3044
|
-
this.#emit("identifyWait", wait);
|
|
3045
|
-
}
|
|
3046
|
-
});
|
|
3047
|
-
const active = chunks.map((shards, id) => ({ id, shards })).filter((chunk) => chunk.shards.length > 0);
|
|
3048
|
-
this.#clusterCount = active.length;
|
|
3049
|
-
this.#hostOf.clear();
|
|
3050
|
-
for (const chunk of active)
|
|
3051
|
-
this.#hostOf.set(chunk.id, placement[chunk.id % placement.length]);
|
|
3052
|
-
for (const [index, chunk] of active.entries()) {
|
|
3053
|
-
if (stale())
|
|
3054
|
-
return;
|
|
3055
|
-
for (const shard of chunk.shards)
|
|
3056
|
-
this.#owner.set(shard, chunk.id);
|
|
3057
|
-
this.#launch(chunk.id, chunk.shards);
|
|
3058
|
-
const gap = this.#options.spawnInterval ?? 500;
|
|
3059
|
-
if (index < active.length - 1 && gap > 0)
|
|
3060
|
-
await this.#pause(gap);
|
|
3061
|
-
}
|
|
3062
|
-
if (stale())
|
|
3063
|
-
return;
|
|
3064
|
-
if (this.#options.waitForReady)
|
|
3065
|
-
await this.ready();
|
|
3066
|
-
if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
|
|
3067
|
-
this.#autoScaleTimer = setInterval(() => {
|
|
3068
|
-
this.checkAutoScale();
|
|
3069
|
-
}, this.#options.autoScaleInterval);
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
get isLeader() {
|
|
3073
|
-
return !this.#options.leader || this.#leading;
|
|
3074
|
-
}
|
|
3075
|
-
async#lead() {
|
|
3076
|
-
const { lock, ttl = 1e4 } = this.#options.leader;
|
|
3077
|
-
const generation = this.#generation;
|
|
3078
|
-
while (!this.#stopping && generation === this.#generation) {
|
|
3079
|
-
const started = Date.now();
|
|
3080
|
-
let held = false;
|
|
3081
|
-
try {
|
|
3082
|
-
held = await withTimeout(lock.acquire(ttl), ttl / 2);
|
|
3083
|
-
} catch (error) {
|
|
3084
|
-
this.#emit("error", asError(error));
|
|
3085
|
-
}
|
|
3086
|
-
if (this.#stopping || generation !== this.#generation) {
|
|
3087
|
-
if (held)
|
|
3088
|
-
lock.release().catch(() => {
|
|
3089
|
-
return;
|
|
3090
|
-
});
|
|
3091
|
-
return;
|
|
3092
|
-
}
|
|
3093
|
-
if (held) {
|
|
3094
|
-
this.#leading = true;
|
|
3095
|
-
this.#lastRenew = started;
|
|
3096
|
-
this.#startRenewing(ttl);
|
|
3097
|
-
this.#emit("leaderAcquired");
|
|
3098
|
-
const lease = this.#options.remote ? this.#options.remote.lease ?? 15000 : 0;
|
|
3099
|
-
if (lease > 0)
|
|
3100
|
-
await this.#pause(lease * 2);
|
|
3101
|
-
return;
|
|
3102
|
-
}
|
|
3103
|
-
await this.#pause(Math.floor(ttl / 2));
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
#startRenewing(ttl) {
|
|
3107
|
-
this.#stopRenewing();
|
|
3108
|
-
const lock = this.#options.leader.lock;
|
|
3109
|
-
this.#renewTimer = setInterval(() => {
|
|
3110
|
-
if (Date.now() - this.#lastRenew > ttl / 2) {
|
|
3111
|
-
this.#standDown("The leader lock could not be renewed.");
|
|
3112
|
-
return;
|
|
3113
|
-
}
|
|
3114
|
-
if (this.#renewing)
|
|
3115
|
-
return;
|
|
3116
|
-
this.#renewing = true;
|
|
3117
|
-
const started = Date.now();
|
|
3118
|
-
lock.acquire(ttl).then((held) => {
|
|
3119
|
-
this.#renewing = false;
|
|
3120
|
-
if (!this.#leading)
|
|
3121
|
-
return;
|
|
3122
|
-
if (held)
|
|
3123
|
-
this.#lastRenew = started;
|
|
3124
|
-
else
|
|
3125
|
-
this.#standDown("Another coordinator holds the leader lock.");
|
|
3126
|
-
}, (error) => {
|
|
3127
|
-
this.#renewing = false;
|
|
3128
|
-
this.#emit("error", asError(error));
|
|
3129
|
-
});
|
|
3130
|
-
}, Math.max(10, Math.floor(ttl / 4)));
|
|
3131
|
-
this.#renewTimer.unref?.();
|
|
3132
|
-
}
|
|
3133
|
-
#stopRenewing() {
|
|
3134
|
-
if (this.#renewTimer)
|
|
3135
|
-
clearInterval(this.#renewTimer);
|
|
3136
|
-
this.#renewTimer = undefined;
|
|
3137
|
-
}
|
|
3138
|
-
async#resign() {
|
|
3139
|
-
this.#stopRenewing();
|
|
3140
|
-
if (!this.#leading)
|
|
3141
|
-
return;
|
|
3142
|
-
this.#leading = false;
|
|
3143
|
-
const { lock, ttl = 1e4 } = this.#options.leader;
|
|
3144
|
-
try {
|
|
3145
|
-
await withTimeout(lock.release(), ttl / 2);
|
|
3146
|
-
} catch (error) {
|
|
3147
|
-
this.#emit("error", asError(error));
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
#standDown(reason) {
|
|
3151
|
-
if (!this.#leading)
|
|
3152
|
-
return;
|
|
3153
|
-
this.#stopRenewing();
|
|
3154
|
-
this.#leading = false;
|
|
3155
|
-
this.#stopping = true;
|
|
3156
|
-
this.#halt();
|
|
3157
|
-
for (const cluster of this.#infos.values()) {
|
|
3158
|
-
cluster.state = "stopped";
|
|
3159
|
-
try {
|
|
3160
|
-
cluster.process.kill("SIGKILL");
|
|
3161
|
-
} catch {}
|
|
3162
|
-
}
|
|
3163
|
-
this.clusters.clear();
|
|
3164
|
-
this.#startPromise = undefined;
|
|
3165
|
-
this.#remote?.stop();
|
|
3166
|
-
this.#stopping = false;
|
|
3167
|
-
this.#emit("leaderLost", reason);
|
|
3168
|
-
this.spawn().catch((error) => this.#emit("error", asError(error)));
|
|
3169
|
-
}
|
|
3170
|
-
async#placeHosts() {
|
|
3171
|
-
const remote = this.#remote;
|
|
3172
|
-
const options = this.#options.remote;
|
|
3173
|
-
if (!remote || !options)
|
|
3174
|
-
return ["local"];
|
|
3175
|
-
remote.start();
|
|
3176
|
-
try {
|
|
3177
|
-
await remote.waitFor(options.hosts, options.minHosts ?? 1, options.joinTimeout ?? 60000);
|
|
3178
|
-
} catch (error) {
|
|
3179
|
-
remote.stop();
|
|
3180
|
-
throw error;
|
|
3181
|
-
}
|
|
3182
|
-
const ids = options.hosts ?? remote.connected();
|
|
3183
|
-
return options.local ? ["local", ...ids] : ids;
|
|
3184
|
-
}
|
|
3185
|
-
get remotePort() {
|
|
3186
|
-
return this.#remote?.port;
|
|
3187
|
-
}
|
|
3188
|
-
hosts() {
|
|
3189
|
-
return this.#remote?.status(this.#options.remote?.hosts) ?? [];
|
|
3190
|
-
}
|
|
3191
|
-
#pause(ms) {
|
|
3192
|
-
return new Promise((resolve) => {
|
|
3193
|
-
const wake = () => {
|
|
3194
|
-
clearTimeout(timer);
|
|
3195
|
-
this.#pauses.delete(wake);
|
|
3196
|
-
resolve();
|
|
3197
|
-
};
|
|
3198
|
-
const timer = setTimeout(wake, ms);
|
|
3199
|
-
this.#pauses.add(wake);
|
|
3200
|
-
});
|
|
3201
|
-
}
|
|
3202
|
-
#launch(id, shards) {
|
|
3203
|
-
const contract = {
|
|
3204
|
-
SHARD_LIST: shards.join(","),
|
|
3205
|
-
SHARD_COUNT: this.shardCount.toString(),
|
|
3206
|
-
CLUSTER_ID: id.toString(),
|
|
3207
|
-
CLUSTER_COUNT: this.#clusterCount.toString(),
|
|
3208
|
-
LUNIBEE_SHARD_BUS: "ipc",
|
|
3209
|
-
[WORKER_MARKER]: "1",
|
|
3210
|
-
LUNIBEE_IDENTIFY: this.#coordinator ? "coordinated" : "local"
|
|
3211
|
-
};
|
|
3212
|
-
const host = this.#hostOf.get(id) ?? "local";
|
|
3213
|
-
const env = {
|
|
3214
|
-
...process.env,
|
|
3215
|
-
...contract
|
|
3216
|
-
};
|
|
3217
|
-
const child = host !== "local" && this.#remote ? this.#remote.createChild(host, id, contract) : this.#options.fork ? this.#options.fork(this.#options.script, env) : fork(this.#options.script, [], {
|
|
3218
|
-
env
|
|
3219
|
-
});
|
|
3220
|
-
const known = this.#infos.get(id);
|
|
3221
|
-
const info = known ?? {
|
|
3222
|
-
id,
|
|
3223
|
-
host,
|
|
3224
|
-
process: child,
|
|
3225
|
-
shards,
|
|
3226
|
-
state: "starting",
|
|
3227
|
-
restarts: 0,
|
|
3228
|
-
startedAt: Date.now()
|
|
3229
|
-
};
|
|
3230
|
-
info.process = child;
|
|
3231
|
-
info.state = "starting";
|
|
3232
|
-
info.startedAt = Date.now();
|
|
3233
|
-
info.readyAt = undefined;
|
|
3234
|
-
this.#infos.set(id, info);
|
|
3235
|
-
this.clusters.set(id, info);
|
|
3236
|
-
this.#allReady = false;
|
|
3237
|
-
child.on("error", (error) => this.#emit("error", new Error(`Cluster ${id} process error: ${error.message}`, {
|
|
3238
|
-
cause: error
|
|
3239
|
-
})));
|
|
3240
|
-
child.on("message", (frame) => this.#onFrame(info, child, frame));
|
|
3241
|
-
child.once("exit", (code, signal) => this.#onExit(info, child, code, signal));
|
|
3242
|
-
const timeout = this.#options.startTimeout;
|
|
3243
|
-
if (timeout) {
|
|
3244
|
-
const timer = setTimeout(() => {
|
|
3245
|
-
this.#startTimers.delete(id);
|
|
3246
|
-
if (info.process !== child || info.state !== "starting")
|
|
3247
|
-
return;
|
|
3248
|
-
this.#emit("clusterStartTimeout", info);
|
|
3249
|
-
try {
|
|
3250
|
-
child.kill("SIGKILL");
|
|
3251
|
-
} catch {}
|
|
3252
|
-
}, timeout);
|
|
3253
|
-
timer.unref?.();
|
|
3254
|
-
this.#startTimers.set(id, timer);
|
|
3255
|
-
}
|
|
3256
|
-
this.#emit("clusterSpawn", info);
|
|
3257
|
-
return info;
|
|
3258
|
-
}
|
|
3259
|
-
#onExit(info, child, code, signal) {
|
|
3260
|
-
if (info.process !== child)
|
|
3261
|
-
return;
|
|
3262
|
-
child.removeAllListeners("message");
|
|
3263
|
-
this.#clearStartTimer(info.id);
|
|
3264
|
-
this.#coordinator?.drop(info.id, `Cluster ${info.id} exited.`);
|
|
3265
|
-
this.#identifyWaiting.delete(info.id);
|
|
3266
|
-
this.#tracker.failOwner(info.id, `Cluster ${info.id} exited.`);
|
|
3267
|
-
this.clusters.delete(info.id);
|
|
3268
|
-
this.#allReady = false;
|
|
3269
|
-
try {
|
|
3270
|
-
this.#options.onClusterExit?.(info, code, signal);
|
|
3271
|
-
} catch {}
|
|
3272
|
-
const expected = this.#stopping || info.state === "stopping" || info.state === "stopped";
|
|
3273
|
-
this.#emit("clusterExit", info, code, signal, expected);
|
|
3274
|
-
if (expected) {
|
|
3275
|
-
info.state = "stopped";
|
|
3276
|
-
return;
|
|
3277
|
-
}
|
|
3278
|
-
this.#announceDown(info.id, info.shards);
|
|
3279
|
-
if (this.#options.restartOnExit === false) {
|
|
3280
|
-
info.state = "failed";
|
|
3281
|
-
this.#rejectReady(info, "restartOnExit is false");
|
|
3282
|
-
return;
|
|
3283
|
-
}
|
|
3284
|
-
const delay = this.#supervisor.next(info.id);
|
|
3285
|
-
if (delay === null) {
|
|
3286
|
-
info.state = "failed";
|
|
3287
|
-
try {
|
|
3288
|
-
this.#options.onGiveUp?.(info);
|
|
3289
|
-
} catch {}
|
|
3290
|
-
this.#emit("clusterGiveUp", info);
|
|
3291
|
-
if (this.#options.reassignOnGiveUp)
|
|
3292
|
-
this.#reassign(info);
|
|
3293
|
-
else
|
|
3294
|
-
this.#rejectReady(info, "restarts exhausted");
|
|
3295
|
-
return;
|
|
3296
|
-
}
|
|
3297
|
-
info.state = "restarting";
|
|
3298
|
-
info.restarts++;
|
|
3299
|
-
this.#emit("clusterRestart", info, { delay, attempt: info.restarts });
|
|
3300
|
-
if (info.restarts >= 3)
|
|
3301
|
-
this.#emit("clusterUnstable", info, {
|
|
3302
|
-
attempt: info.restarts,
|
|
3303
|
-
delay
|
|
3304
|
-
});
|
|
3305
|
-
this.#restartLater(info, delay);
|
|
3306
|
-
}
|
|
3307
|
-
#restartLater(info, delay) {
|
|
3308
|
-
const timer = setTimeout(() => {
|
|
3309
|
-
this.#restartTimers.delete(timer);
|
|
3310
|
-
this.#fencing.delete(info.id);
|
|
3311
|
-
if (this.#stopping || info.state !== "restarting")
|
|
3312
|
-
return;
|
|
3313
|
-
const wait = this.#relocate(info);
|
|
3314
|
-
if (wait > 0)
|
|
3315
|
-
this.#fencing.set(info.id, this.#restartLater(info, wait));
|
|
3316
|
-
else
|
|
3317
|
-
this.#launch(info.id, info.shards);
|
|
3318
|
-
}, delay);
|
|
3319
|
-
timer.unref?.();
|
|
3320
|
-
this.#restartTimers.add(timer);
|
|
3321
|
-
return timer;
|
|
3322
|
-
}
|
|
3323
|
-
#hostJoined(hostId) {
|
|
3324
|
-
this.#emit("hostJoin", hostId);
|
|
3325
|
-
for (const [id, timer] of [...this.#fencing]) {
|
|
3326
|
-
const info = this.#infos.get(id);
|
|
3327
|
-
if (this.#hostOf.get(id) !== hostId || !info)
|
|
3328
|
-
continue;
|
|
3329
|
-
clearTimeout(timer);
|
|
3330
|
-
this.#restartTimers.delete(timer);
|
|
3331
|
-
this.#fencing.delete(id);
|
|
3332
|
-
if (!this.#stopping && info.state === "restarting")
|
|
3333
|
-
this.#launch(info.id, info.shards);
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
#relocate(info) {
|
|
3337
|
-
const remote = this.#remote;
|
|
3338
|
-
const host = this.#hostOf.get(info.id) ?? "local";
|
|
3339
|
-
if (!remote || this.#options.remote?.moveClusters === false || host === "local" || remote.isConnected(host))
|
|
3340
|
-
return 0;
|
|
3341
|
-
const wait = remote.fencedAt(host) - Date.now();
|
|
3342
|
-
if (wait > 0)
|
|
3343
|
-
return wait;
|
|
3344
|
-
const places = this.#options.remote?.local ? ["local", ...remote.connected()] : remote.connected();
|
|
3345
|
-
if (places.length === 0)
|
|
3346
|
-
return 0;
|
|
3347
|
-
const load = new Map(places.map((place) => [place, 0]));
|
|
3348
|
-
for (const [cluster, place] of this.#hostOf) {
|
|
3349
|
-
const state = this.#infos.get(cluster)?.state;
|
|
3350
|
-
if (load.has(place) && state !== "failed" && state !== "stopped")
|
|
3351
|
-
load.set(place, load.get(place) + 1);
|
|
3352
|
-
}
|
|
3353
|
-
const target = places.reduce((best, place) => load.get(place) < load.get(best) ? place : best);
|
|
3354
|
-
this.#hostOf.set(info.id, target);
|
|
3355
|
-
info.host = target;
|
|
3356
|
-
this.#emit("clusterMoved", info, { from: host, to: target });
|
|
3357
|
-
return 0;
|
|
3358
|
-
}
|
|
3359
|
-
async#reassign(failed) {
|
|
3360
|
-
const shardIds = [...failed.shards];
|
|
3361
|
-
const target = [...this.#infos.values()].filter((cluster) => cluster.state === "ready").sort((a, b) => a.shards.length - b.shards.length)[0];
|
|
3362
|
-
if (!target) {
|
|
3363
|
-
this.#emit("error", new Error(`No ready cluster can take over shards [${shardIds}] of cluster ${failed.id}.`));
|
|
3364
|
-
this.#rejectReady(failed, "no ready cluster to take them");
|
|
3365
|
-
return;
|
|
3366
|
-
}
|
|
3367
|
-
try {
|
|
3368
|
-
await this.requestCluster(target.id, REQUEST_ASSIGN, { shardIds }, { timeoutMs: this.#options.startTimeout ?? 120000 });
|
|
3369
|
-
} catch (error) {
|
|
3370
|
-
this.#emit("error", new Error(`Cluster ${target.id} could not take over shards [${shardIds}]: ${error instanceof Error ? error.message : String(error)}`));
|
|
3371
|
-
this.#rejectReady(failed, "reassignment failed");
|
|
3372
|
-
return;
|
|
3373
|
-
}
|
|
3374
|
-
if (this.#stopping || failed.state !== "failed")
|
|
3375
|
-
return;
|
|
3376
|
-
for (const shard of shardIds)
|
|
3377
|
-
this.#owner.set(shard, target.id);
|
|
3378
|
-
target.shards.push(...shardIds);
|
|
3379
|
-
failed.shards = [];
|
|
3380
|
-
this.#emit("shardsReassigned", {
|
|
3381
|
-
shardIds,
|
|
3382
|
-
from: failed.id,
|
|
3383
|
-
to: target.id
|
|
3384
|
-
});
|
|
3385
|
-
this.#checkReady();
|
|
3386
|
-
}
|
|
3387
|
-
#clearStartTimer(id) {
|
|
3388
|
-
const timer = this.#startTimers.get(id);
|
|
3389
|
-
if (timer)
|
|
3390
|
-
clearTimeout(timer);
|
|
3391
|
-
this.#startTimers.delete(id);
|
|
3392
|
-
}
|
|
3393
|
-
#sendFrame(cluster, frame) {
|
|
3394
|
-
const child = cluster.process;
|
|
3395
|
-
if (!isLive(cluster.state) || typeof child.send !== "function" || !child.connected)
|
|
3396
|
-
return false;
|
|
3397
|
-
try {
|
|
3398
|
-
child.send(frame);
|
|
3399
|
-
return true;
|
|
3400
|
-
} catch {
|
|
3401
|
-
return false;
|
|
3402
|
-
}
|
|
3403
|
-
}
|
|
3404
|
-
#announceDown(exited, shards) {
|
|
3405
|
-
for (const [id, cluster] of this.clusters)
|
|
3406
|
-
if (id !== exited)
|
|
3407
|
-
this.#sendFrame(cluster, { [PEER_DOWN_FRAME]: shards });
|
|
3408
|
-
}
|
|
3409
|
-
#onFrame(info, child, frame) {
|
|
3410
|
-
if (info.process !== child)
|
|
3411
|
-
return;
|
|
3412
|
-
if (!frame || typeof frame !== "object")
|
|
3413
|
-
return;
|
|
3414
|
-
if (BUS_FRAME in frame) {
|
|
3415
|
-
this.#onBus(info, frame[BUS_FRAME]);
|
|
3416
|
-
return;
|
|
3417
|
-
}
|
|
3418
|
-
if (CLUSTER_FRAME in frame) {
|
|
3419
|
-
const body = parseChildFrame(frame[CLUSTER_FRAME]);
|
|
3420
|
-
if (body)
|
|
3421
|
-
this.#onControl(info, child, body);
|
|
3422
|
-
else
|
|
3423
|
-
this.#emit("error", new Error(`Cluster ${info.id} sent a malformed control frame.`));
|
|
3424
|
-
}
|
|
3425
|
-
}
|
|
3426
|
-
#onControl(info, child, frame) {
|
|
3427
|
-
switch (frame.kind) {
|
|
3428
|
-
case "ready": {
|
|
3429
|
-
if (!frame.shardIds.every((id) => info.shards.includes(id))) {
|
|
3430
|
-
this.#emit("error", new Error(`Cluster ${info.id} reported shards it does not own: [${frame.shardIds}].`));
|
|
3431
|
-
return;
|
|
3432
|
-
}
|
|
3433
|
-
this.#clearStartTimer(info.id);
|
|
3434
|
-
info.state = "ready";
|
|
3435
|
-
info.readyAt = info.lastReadyAt = Date.now();
|
|
3436
|
-
this.#emit("clusterReady", info);
|
|
3437
|
-
this.#checkReady();
|
|
3438
|
-
return;
|
|
3439
|
-
}
|
|
3440
|
-
case "identify": {
|
|
3441
|
-
const reply = (body) => {
|
|
3442
|
-
if (info.process === child)
|
|
3443
|
-
this.#sendFrame(info, { [CLUSTER_FRAME]: body });
|
|
3444
|
-
};
|
|
3445
|
-
if (!this.#coordinator) {
|
|
3446
|
-
reply({
|
|
3447
|
-
kind: "identify-deny",
|
|
3448
|
-
id: frame.id,
|
|
3449
|
-
error: "IDENTIFY coordination is off."
|
|
3450
|
-
});
|
|
3451
|
-
return;
|
|
3452
|
-
}
|
|
3453
|
-
if (!info.shards.includes(frame.shardId)) {
|
|
3454
|
-
reply({
|
|
3455
|
-
kind: "identify-deny",
|
|
3456
|
-
id: frame.id,
|
|
3457
|
-
error: `Shard ${frame.shardId} does not belong to cluster ${info.id}.`
|
|
3458
|
-
});
|
|
3459
|
-
return;
|
|
3460
|
-
}
|
|
3461
|
-
let waiting = this.#identifyWaiting.get(info.id);
|
|
3462
|
-
if (!waiting)
|
|
3463
|
-
this.#identifyWaiting.set(info.id, waiting = new Set);
|
|
3464
|
-
if (waiting.has(frame.shardId)) {
|
|
3465
|
-
reply({
|
|
3466
|
-
kind: "identify-deny",
|
|
3467
|
-
id: frame.id,
|
|
3468
|
-
error: `Shard ${frame.shardId} is already waiting for a slot.`
|
|
3469
|
-
});
|
|
3470
|
-
return;
|
|
3471
|
-
}
|
|
3472
|
-
waiting.add(frame.shardId);
|
|
3473
|
-
const settled = () => {
|
|
3474
|
-
waiting.delete(frame.shardId);
|
|
3475
|
-
};
|
|
3476
|
-
this.#coordinator.acquire(info.id, frame.shardId).then(() => {
|
|
3477
|
-
settled();
|
|
3478
|
-
reply({ kind: "identify-grant", id: frame.id });
|
|
3479
|
-
}, (error) => {
|
|
3480
|
-
settled();
|
|
3481
|
-
reply({
|
|
3482
|
-
kind: "identify-deny",
|
|
3483
|
-
id: frame.id,
|
|
3484
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3485
|
-
});
|
|
3486
|
-
});
|
|
3487
|
-
return;
|
|
3488
|
-
}
|
|
3489
|
-
case "identify-done":
|
|
3490
|
-
this.#coordinator?.release(info.id, frame.shardId);
|
|
3491
|
-
return;
|
|
3492
|
-
case "reply": {
|
|
3493
|
-
const tooBig = frame.error === undefined && this.#tooBig(frame.result);
|
|
3494
|
-
this.#tracker.answer(frame.id, `c:${info.id}`, info.id, {
|
|
3495
|
-
...frame.error !== undefined ? { error: frame.error } : tooBig ? {
|
|
3496
|
-
error: `Cluster ${info.id} answered with more than ${this.#maxBytes} bytes.`
|
|
3497
|
-
} : { result: frame.result }
|
|
3498
|
-
});
|
|
3499
|
-
return;
|
|
3500
|
-
}
|
|
3501
|
-
case "health":
|
|
3502
|
-
if (this.#tooBig(frame.report)) {
|
|
3503
|
-
this.#emit("error", new Error(`Cluster ${info.id} sent a health report of more than ${this.#maxBytes} bytes.`));
|
|
3504
|
-
return;
|
|
3505
|
-
}
|
|
3506
|
-
info.report = frame.report;
|
|
3507
|
-
return;
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
get #maxBytes() {
|
|
3511
|
-
return this.#options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
|
|
3512
|
-
}
|
|
3513
|
-
#tooBig(value) {
|
|
3514
|
-
return payloadSize(value) > this.#maxBytes;
|
|
3515
|
-
}
|
|
3516
|
-
#assertSize(data, what) {
|
|
3517
|
-
if (this.#tooBig(data))
|
|
3518
|
-
throw new RangeError(`${what} is larger than maxMessageBytes (${this.#maxBytes}).`);
|
|
3519
|
-
}
|
|
3520
|
-
#onBus(from, raw) {
|
|
3521
|
-
let message = raw;
|
|
3522
|
-
if (!message || typeof message !== "object" || typeof message.type !== "string" || message.type.length === 0 || message.type.length > 256 || message.id === undefined || typeof message.id !== "string" || message.id.length > 256 || !Number.isInteger(message.source) || message.target !== null && !Number.isInteger(message.target))
|
|
3523
|
-
return;
|
|
3524
|
-
if (!from.shards.includes(message.source))
|
|
3525
|
-
return;
|
|
3526
|
-
const { type, id, source, target, data, expectsReply } = message;
|
|
3527
|
-
message = {
|
|
3528
|
-
type,
|
|
3529
|
-
id,
|
|
3530
|
-
source,
|
|
3531
|
-
target,
|
|
3532
|
-
data,
|
|
3533
|
-
...expectsReply === undefined ? {} : { expectsReply }
|
|
3534
|
-
};
|
|
3535
|
-
if (this.#tooBig(message.data)) {
|
|
3536
|
-
this.#emit("error", new Error(`Cluster ${from.id} sent a shard-bus message of more than ${this.#maxBytes} bytes; it was not relayed.`));
|
|
3537
|
-
return;
|
|
3538
|
-
}
|
|
3539
|
-
const frame = { [BUS_FRAME]: message };
|
|
3540
|
-
if (message.target === PARENT_ID) {
|
|
3541
|
-
const reply = message.data;
|
|
3542
|
-
if (message.type === REPLY_TYPE && reply && typeof reply.requestId === "string")
|
|
3543
|
-
this.#tracker.answer(reply.requestId, `s:${message.source}`, from.id, reply.error !== undefined ? { error: String(reply.error) } : { result: reply.result });
|
|
3544
|
-
return;
|
|
3545
|
-
}
|
|
3546
|
-
if (message.target === null) {
|
|
3547
|
-
for (const [id, cluster] of this.clusters)
|
|
3548
|
-
if (id !== from.id)
|
|
3549
|
-
this.#sendFrame(cluster, frame);
|
|
3550
|
-
return;
|
|
3551
|
-
}
|
|
3552
|
-
const owner = this.clusterForShard(message.target);
|
|
3553
|
-
if (!owner || owner.id === from.id)
|
|
3554
|
-
return;
|
|
3555
|
-
if (!this.#sendFrame(owner, frame))
|
|
3556
|
-
this.#sendFrame(from, {
|
|
3557
|
-
[PEER_DOWN_FRAME]: [message.target]
|
|
3558
|
-
});
|
|
3559
|
-
}
|
|
3560
|
-
async request(shardId, type, data, options = {}) {
|
|
3561
|
-
this.#assertSize(data, "The request data");
|
|
3562
|
-
const owner = this.clusterForShard(shardId);
|
|
3563
|
-
if (!owner)
|
|
3564
|
-
throw new RangeError(`Shard ${shardId} is not part of this deployment.`);
|
|
3565
|
-
const { id, done } = this.#tracker.begin({
|
|
3566
|
-
keys: new Map([[`s:${shardId}`, owner.id]]),
|
|
3567
|
-
timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
|
|
3568
|
-
signal: options.signal
|
|
3569
|
-
});
|
|
3570
|
-
const sent = this.#sendFrame(owner, {
|
|
3571
|
-
[BUS_FRAME]: {
|
|
3572
|
-
source: PARENT_ID,
|
|
3573
|
-
target: shardId,
|
|
3574
|
-
type,
|
|
3575
|
-
data,
|
|
3576
|
-
id,
|
|
3577
|
-
expectsReply: true
|
|
3578
|
-
}
|
|
3579
|
-
});
|
|
3580
|
-
if (!sent) {
|
|
3581
|
-
this.#tracker.failOwner(owner.id, `Shard ${shardId} is down: cluster ${owner.id} is ${owner.state}.`);
|
|
3582
|
-
}
|
|
3583
|
-
const { replies } = await done;
|
|
3584
|
-
const reply = replies[0];
|
|
3585
|
-
if (!reply)
|
|
3586
|
-
throw new Error(`Shard ${shardId} did not reply to "${type}" in time.`);
|
|
3587
|
-
if (reply.error !== undefined)
|
|
3588
|
-
throw new Error(reply.error);
|
|
3589
|
-
return reply.result;
|
|
3590
|
-
}
|
|
3591
|
-
async broadcast(type, data, options = {}) {
|
|
3592
|
-
this.#assertSize(data, "The request data");
|
|
3593
|
-
const keys = new Map;
|
|
3594
|
-
const down = [];
|
|
3595
|
-
for (const [shard, clusterId] of this.#owner) {
|
|
3596
|
-
const cluster = this.#infos.get(clusterId);
|
|
3597
|
-
if (cluster && isLive(cluster.state))
|
|
3598
|
-
keys.set(`s:${shard}`, clusterId);
|
|
3599
|
-
else
|
|
3600
|
-
down.push(shard);
|
|
3601
|
-
}
|
|
3602
|
-
const { id, done } = this.#tracker.begin({
|
|
3603
|
-
keys,
|
|
3604
|
-
timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
|
|
3605
|
-
signal: options.signal
|
|
3606
|
-
});
|
|
3607
|
-
const frame = {
|
|
3608
|
-
[BUS_FRAME]: {
|
|
3609
|
-
source: PARENT_ID,
|
|
3610
|
-
target: null,
|
|
3611
|
-
type,
|
|
3612
|
-
data,
|
|
3613
|
-
id,
|
|
3614
|
-
expectsReply: true
|
|
3615
|
-
}
|
|
3616
|
-
};
|
|
3617
|
-
for (const cluster of this.clusters.values())
|
|
3618
|
-
if (!this.#sendFrame(cluster, frame))
|
|
3619
|
-
this.#tracker.failOwner(cluster.id, `Cluster ${cluster.id} is ${cluster.state}.`);
|
|
3620
|
-
const { replies, missing } = await done;
|
|
3621
|
-
return {
|
|
3622
|
-
replies: replies.map((reply) => ({
|
|
3623
|
-
shardId: Number(reply.key.slice(2)),
|
|
3624
|
-
...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
|
|
3625
|
-
})),
|
|
3626
|
-
missing: [
|
|
3627
|
-
...down,
|
|
3628
|
-
...missing.map((key) => Number(key.slice(2)))
|
|
3629
|
-
].sort((a, b) => a - b)
|
|
3630
|
-
};
|
|
3631
|
-
}
|
|
3632
|
-
async requestCluster(clusterId, type, data, options = {}) {
|
|
3633
|
-
this.#assertSize(data, "The request data");
|
|
3634
|
-
const cluster = this.#infos.get(clusterId);
|
|
3635
|
-
if (!cluster)
|
|
3636
|
-
throw new RangeError(`Cluster ${clusterId} does not exist.`);
|
|
3637
|
-
const { id, done } = this.#tracker.begin({
|
|
3638
|
-
keys: new Map([[`c:${clusterId}`, clusterId]]),
|
|
3639
|
-
timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
|
|
3640
|
-
signal: options.signal
|
|
3641
|
-
});
|
|
3642
|
-
if (!this.#sendFrame(cluster, {
|
|
3643
|
-
[CLUSTER_FRAME]: { kind: "request", id, type, data }
|
|
3644
|
-
}))
|
|
3645
|
-
this.#tracker.failOwner(clusterId, `Cluster ${clusterId} is ${cluster.state}.`);
|
|
3646
|
-
const { replies } = await done;
|
|
3647
|
-
const reply = replies[0];
|
|
3648
|
-
if (!reply)
|
|
3649
|
-
throw new Error(`Cluster ${clusterId} did not reply to "${type}" in time.`);
|
|
3650
|
-
if (reply.error !== undefined)
|
|
3651
|
-
throw new Error(reply.error);
|
|
3652
|
-
return reply.result;
|
|
3653
|
-
}
|
|
3654
|
-
ready(timeoutMs) {
|
|
3655
|
-
if (this.#isReady())
|
|
3656
|
-
return Promise.resolve();
|
|
3657
|
-
return new Promise((resolve, reject) => {
|
|
3658
|
-
const waiter = {
|
|
3659
|
-
resolve: () => {
|
|
3660
|
-
if (waiter.timer)
|
|
3661
|
-
clearTimeout(waiter.timer);
|
|
3662
|
-
this.#readyWaiters.delete(waiter);
|
|
3663
|
-
resolve();
|
|
3664
|
-
},
|
|
3665
|
-
reject: (error) => {
|
|
3666
|
-
if (waiter.timer)
|
|
3667
|
-
clearTimeout(waiter.timer);
|
|
3668
|
-
this.#readyWaiters.delete(waiter);
|
|
3669
|
-
reject(error);
|
|
3670
|
-
}
|
|
3671
|
-
};
|
|
3672
|
-
if (timeoutMs !== undefined)
|
|
3673
|
-
waiter.timer = setTimeout(() => waiter.reject(new Error(`Clusters were not all ready within ${timeoutMs}ms.`)), timeoutMs);
|
|
3674
|
-
this.#readyWaiters.add(waiter);
|
|
3675
|
-
});
|
|
3676
|
-
}
|
|
3677
|
-
#isReady() {
|
|
3678
|
-
let ready = 0;
|
|
3679
|
-
for (const cluster of this.#infos.values()) {
|
|
3680
|
-
if (cluster.state === "ready")
|
|
3681
|
-
ready++;
|
|
3682
|
-
else if (!(cluster.state === "failed" && cluster.shards.length === 0))
|
|
3683
|
-
return false;
|
|
3684
|
-
}
|
|
3685
|
-
return ready > 0;
|
|
3686
|
-
}
|
|
3687
|
-
#rejectReady(failed, why) {
|
|
3688
|
-
if (failed.shards.length === 0)
|
|
3689
|
-
return;
|
|
3690
|
-
const error = new Error(`Cluster ${failed.id} failed and shards [${failed.shards}] have no owner (${why}).`);
|
|
3691
|
-
for (const waiter of [...this.#readyWaiters])
|
|
3692
|
-
waiter.reject(error);
|
|
3693
|
-
}
|
|
3694
|
-
#checkReady() {
|
|
3695
|
-
if (!this.#isReady())
|
|
3696
|
-
return;
|
|
3697
|
-
for (const waiter of [...this.#readyWaiters])
|
|
3698
|
-
waiter.resolve();
|
|
3699
|
-
if (!this.#allReady) {
|
|
3700
|
-
this.#allReady = true;
|
|
3701
|
-
this.#emit("ready");
|
|
3702
|
-
}
|
|
3703
|
-
}
|
|
3704
|
-
health() {
|
|
3705
|
-
const now = Date.now();
|
|
3706
|
-
const clusters = [...this.#infos.values()].sort((a, b) => a.id - b.id).map((cluster) => ({
|
|
3707
|
-
id: cluster.id,
|
|
3708
|
-
state: cluster.state,
|
|
3709
|
-
...cluster.process.pid !== undefined && isLive(cluster.state) ? { pid: cluster.process.pid } : {},
|
|
3710
|
-
host: cluster.host,
|
|
3711
|
-
shardIds: [...cluster.shards],
|
|
3712
|
-
uptimeMs: isLive(cluster.state) ? now - cluster.startedAt : 0,
|
|
3713
|
-
restarts: cluster.restarts,
|
|
3714
|
-
startedAt: cluster.startedAt,
|
|
3715
|
-
...cluster.readyAt !== undefined ? { readyAt: cluster.readyAt } : {},
|
|
3716
|
-
...cluster.lastReadyAt !== undefined ? { lastReadyAt: cluster.lastReadyAt } : {},
|
|
3717
|
-
shards: cluster.report?.shards ?? [],
|
|
3718
|
-
...cluster.report?.metrics ? { metrics: cluster.report.metrics } : {},
|
|
3719
|
-
...cluster.report ? { reportedAt: cluster.report.reportedAt } : {}
|
|
3720
|
-
}));
|
|
3721
|
-
const missingShards = [...this.#owner].filter(([, id]) => this.#infos.get(id)?.state !== "ready").map(([shard]) => shard).sort((a, b) => a - b);
|
|
3722
|
-
const states = clusters.map((cluster) => cluster.state);
|
|
3723
|
-
const state = this.#stopping ? "stopping" : clusters.length === 0 ? this.#wasStarted ? "stopped" : "idle" : states.every((s) => s === "stopped") ? "stopped" : states.every((s) => s === "ready") ? "ready" : states.some((s) => s === "restarting" || s === "failed") ? "degraded" : "starting";
|
|
3724
|
-
return {
|
|
3725
|
-
state,
|
|
3726
|
-
totalShards: this.shardCount,
|
|
3727
|
-
clusters,
|
|
3728
|
-
readyClusters: states.filter((s) => s === "ready").length,
|
|
3729
|
-
missingShards
|
|
3730
|
-
};
|
|
3731
|
-
}
|
|
3732
|
-
async refreshHealth(options = {}) {
|
|
3733
|
-
await Promise.all([...this.clusters.values()].map(async (cluster) => {
|
|
3734
|
-
try {
|
|
3735
|
-
cluster.report = await this.requestCluster(cluster.id, REQUEST_HEALTH, { metrics: options.metrics === true }, { timeoutMs: options.timeoutMs ?? 2000 });
|
|
3736
|
-
} catch {}
|
|
3737
|
-
}));
|
|
3738
|
-
return this.health();
|
|
3739
|
-
}
|
|
3740
|
-
async checkAutoScale() {
|
|
3741
|
-
if (!this.#auto)
|
|
3742
|
-
return;
|
|
3743
|
-
try {
|
|
3744
|
-
const recommended = await this.fetchRecommendedShardCount();
|
|
3745
|
-
if (recommended !== this.shardCount) {
|
|
3746
|
-
await this.respawn(recommended);
|
|
3747
|
-
}
|
|
3748
|
-
} catch (error) {
|
|
3749
|
-
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
3750
|
-
this.#options.onAutoScaleError?.(normalized);
|
|
3751
|
-
}
|
|
3752
|
-
}
|
|
3753
|
-
async respawn(newShardCount) {
|
|
3754
|
-
this.#options.shardCount = newShardCount;
|
|
3755
|
-
await this.#stop(this.#options.shutdownTimeout ?? 5000, true);
|
|
3756
|
-
this.#supervisor.reset();
|
|
3757
|
-
await this.spawn();
|
|
3758
|
-
}
|
|
3759
|
-
shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
|
|
3760
|
-
return this.#stop(timeoutMs, false);
|
|
3761
|
-
}
|
|
3762
|
-
#stop(timeoutMs, keepRemote) {
|
|
3763
|
-
this.#stopPromise ??= this.#shutdown(timeoutMs, keepRemote).finally(() => {
|
|
3764
|
-
this.#stopPromise = undefined;
|
|
3765
|
-
});
|
|
3766
|
-
return this.#stopPromise;
|
|
3767
|
-
}
|
|
3768
|
-
async#shutdown(timeoutMs, keepRemote) {
|
|
3769
|
-
this.#stopping = true;
|
|
3770
|
-
this.#halt();
|
|
3771
|
-
await this.#startPromise?.catch(() => {
|
|
3772
|
-
return;
|
|
3773
|
-
});
|
|
3774
|
-
await Promise.all([...this.clusters.values()].map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
|
|
3775
|
-
for (const cluster of this.#infos.values())
|
|
3776
|
-
cluster.state = "stopped";
|
|
3777
|
-
this.clusters.clear();
|
|
3778
|
-
this.#startPromise = undefined;
|
|
3779
|
-
if (!keepRemote) {
|
|
3780
|
-
this.#remote?.stop();
|
|
3781
|
-
await this.#resign();
|
|
3782
|
-
}
|
|
3783
|
-
this.#stopping = false;
|
|
3784
|
-
}
|
|
3785
|
-
#halt() {
|
|
3786
|
-
this.#generation++;
|
|
3787
|
-
for (const timer of this.#restartTimers)
|
|
3788
|
-
clearTimeout(timer);
|
|
3789
|
-
this.#restartTimers.clear();
|
|
3790
|
-
this.#fencing.clear();
|
|
3791
|
-
for (const id of [...this.#startTimers.keys()])
|
|
3792
|
-
this.#clearStartTimer(id);
|
|
3793
|
-
for (const wake of [...this.#pauses])
|
|
3794
|
-
wake();
|
|
3795
|
-
if (this.#autoScaleTimer) {
|
|
3796
|
-
clearInterval(this.#autoScaleTimer);
|
|
3797
|
-
this.#autoScaleTimer = undefined;
|
|
3798
|
-
}
|
|
3799
|
-
this.#coordinator?.close();
|
|
3800
|
-
this.#coordinator = undefined;
|
|
3801
|
-
this.#remote?.abortWaits();
|
|
3802
|
-
this.#tracker.close();
|
|
3803
|
-
for (const waiter of [...this.#readyWaiters])
|
|
3804
|
-
waiter.reject(new Error("The cluster manager is stopping."));
|
|
3805
|
-
}
|
|
3806
|
-
async#shutdownCluster(cluster, timeoutMs) {
|
|
3807
|
-
const child = cluster.process;
|
|
3808
|
-
cluster.state = "stopping";
|
|
3809
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
3810
|
-
return;
|
|
3811
|
-
await new Promise((resolve) => {
|
|
3812
|
-
let timer;
|
|
3813
|
-
const finish = () => {
|
|
3814
|
-
if (timer)
|
|
3815
|
-
clearTimeout(timer);
|
|
3816
|
-
resolve();
|
|
3817
|
-
};
|
|
3818
|
-
child.once("exit", finish);
|
|
3819
|
-
try {
|
|
3820
|
-
child.kill("SIGTERM");
|
|
3821
|
-
} catch {
|
|
3822
|
-
child.off("exit", finish);
|
|
3823
|
-
finish();
|
|
3824
|
-
return;
|
|
3825
|
-
}
|
|
3826
|
-
timer = setTimeout(() => {
|
|
3827
|
-
try {
|
|
3828
|
-
child.kill("SIGKILL");
|
|
3829
|
-
} catch {}
|
|
3830
|
-
}, timeoutMs);
|
|
3831
|
-
});
|
|
3832
|
-
}
|
|
3833
|
-
killAll() {
|
|
3834
|
-
this.#stopping = true;
|
|
3835
|
-
this.#halt();
|
|
3836
|
-
for (const cluster of this.#infos.values()) {
|
|
3837
|
-
cluster.state = "stopped";
|
|
3838
|
-
try {
|
|
3839
|
-
cluster.process.kill();
|
|
3840
|
-
} catch {}
|
|
3841
|
-
}
|
|
3842
|
-
this.clusters.clear();
|
|
3843
|
-
this.#startPromise = undefined;
|
|
3844
|
-
this.#remote?.stop();
|
|
3845
|
-
this.#resign();
|
|
3846
|
-
this.#stopping = false;
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
|
-
function withTimeout(promise, ms) {
|
|
3850
|
-
let timer;
|
|
3851
|
-
const timeout = new Promise((_, reject) => {
|
|
3852
|
-
timer = setTimeout(() => reject(new Error(`The leader lock did not answer within ${ms} ms.`)), ms);
|
|
3853
|
-
});
|
|
3854
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
3855
|
-
}
|
|
3856
|
-
function asError(error) {
|
|
3857
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
3858
|
-
}
|
|
3859
|
-
// packages/sharding/src/host.ts
|
|
3860
|
-
import { fork as fork2 } from "child_process";
|
|
3861
|
-
class ClusterHost {
|
|
3862
|
-
#options;
|
|
3863
|
-
#running = new Map;
|
|
3864
|
-
#listeners = new Map;
|
|
3865
|
-
#socket;
|
|
3866
|
-
#urls;
|
|
3867
|
-
#urlIndex = 0;
|
|
3868
|
-
#unreachable = 0;
|
|
3869
|
-
#accepted = false;
|
|
3870
|
-
#lease = 15000;
|
|
3871
|
-
#lastRx = 0;
|
|
3872
|
-
#watchdog;
|
|
3873
|
-
#retry;
|
|
3874
|
-
#delay;
|
|
3875
|
-
#busyAttempts = 0;
|
|
3876
|
-
#closed = true;
|
|
3877
|
-
#connected = false;
|
|
3878
|
-
#firstConnect;
|
|
3879
|
-
constructor(options) {
|
|
3880
|
-
if (!HOST_ID.test(options.id))
|
|
3881
|
-
throw new RangeError("id must be 1-64 letters, digits, '.', '_' or '-'.");
|
|
3882
|
-
if (!options.secret || options.secret.length < 16)
|
|
3883
|
-
throw new RangeError("secret must be at least 16 characters.");
|
|
3884
|
-
if (!options.script?.trim())
|
|
3885
|
-
throw new TypeError("A worker script path is required.");
|
|
3886
|
-
const urls = Array.isArray(options.url) ? [...options.url] : [options.url];
|
|
3887
|
-
if (urls.length === 0 || urls.length > 16)
|
|
3888
|
-
throw new TypeError("url must list 1 to 16 coordinators.");
|
|
3889
|
-
for (const raw of urls) {
|
|
3890
|
-
let url;
|
|
3891
|
-
try {
|
|
3892
|
-
url = new URL(raw);
|
|
3893
|
-
} catch {
|
|
3894
|
-
throw new TypeError("url must be an absolute ws:// or wss:// URL.");
|
|
3895
|
-
}
|
|
3896
|
-
if (url.protocol !== "ws:" && url.protocol !== "wss:")
|
|
3897
|
-
throw new TypeError("url must start with ws:// or wss://.");
|
|
3898
|
-
if (url.username || url.password)
|
|
3899
|
-
throw new TypeError("url must not contain credentials.");
|
|
3900
|
-
if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
|
|
3901
|
-
throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
|
|
3902
|
-
}
|
|
3903
|
-
this.#urls = urls;
|
|
3904
|
-
this.#options = options;
|
|
3905
|
-
this.#delay = options.reconnectDelay ?? 1000;
|
|
3906
|
-
}
|
|
3907
|
-
get connected() {
|
|
3908
|
-
return this.#connected;
|
|
3909
|
-
}
|
|
3910
|
-
clusters() {
|
|
3911
|
-
return [...this.#running.keys()].sort((a, b) => a - b);
|
|
3912
|
-
}
|
|
3913
|
-
on(event, listener) {
|
|
3914
|
-
let set = this.#listeners.get(event);
|
|
3915
|
-
if (!set)
|
|
3916
|
-
this.#listeners.set(event, set = new Set);
|
|
3917
|
-
set.add(listener);
|
|
3918
|
-
return this;
|
|
3919
|
-
}
|
|
3920
|
-
off(event, listener) {
|
|
3921
|
-
this.#listeners.get(event)?.delete(listener);
|
|
3922
|
-
return this;
|
|
3923
|
-
}
|
|
3924
|
-
#emit(event, ...args) {
|
|
3925
|
-
for (const listener of this.#listeners.get(event) ?? [])
|
|
3926
|
-
try {
|
|
3927
|
-
listener(...args);
|
|
3928
|
-
} catch {}
|
|
3929
|
-
}
|
|
3930
|
-
connect() {
|
|
3931
|
-
if (!this.#closed)
|
|
3932
|
-
return Promise.resolve();
|
|
3933
|
-
this.#closed = false;
|
|
3934
|
-
this.#busyAttempts = 0;
|
|
3935
|
-
this.#unreachable = 0;
|
|
3936
|
-
return new Promise((resolve, reject) => {
|
|
3937
|
-
this.#firstConnect = { resolve, reject };
|
|
3938
|
-
this.#open();
|
|
3939
|
-
});
|
|
3940
|
-
}
|
|
3941
|
-
close() {
|
|
3942
|
-
this.#closed = true;
|
|
3943
|
-
if (this.#retry)
|
|
3944
|
-
clearTimeout(this.#retry);
|
|
3945
|
-
this.#retry = undefined;
|
|
3946
|
-
this.#fence();
|
|
3947
|
-
if (this.#watchdog)
|
|
3948
|
-
clearInterval(this.#watchdog);
|
|
3949
|
-
this.#watchdog = undefined;
|
|
3950
|
-
this.#connected = false;
|
|
3951
|
-
const socket = this.#socket;
|
|
3952
|
-
this.#socket = undefined;
|
|
3953
|
-
this.#firstConnect?.reject(new Error("The host was closed."));
|
|
3954
|
-
this.#firstConnect = undefined;
|
|
3955
|
-
if (!socket || socket.readyState === WebSocket.CLOSED)
|
|
3956
|
-
return Promise.resolve();
|
|
3957
|
-
return new Promise((resolve) => {
|
|
3958
|
-
socket.addEventListener("close", () => resolve(), { once: true });
|
|
3959
|
-
try {
|
|
3960
|
-
socket.close(1000, "Host closed.");
|
|
3961
|
-
} catch {
|
|
3962
|
-
resolve();
|
|
3963
|
-
}
|
|
3964
|
-
});
|
|
3965
|
-
}
|
|
3966
|
-
#open() {
|
|
3967
|
-
this.#accepted = false;
|
|
3968
|
-
const socket = new WebSocket(this.#urls[this.#urlIndex]);
|
|
3969
|
-
this.#socket = socket;
|
|
3970
|
-
socket.addEventListener("message", (event) => this.#message(socket, event.data));
|
|
3971
|
-
socket.addEventListener("error", () => {});
|
|
3972
|
-
socket.addEventListener("close", (event) => {
|
|
3973
|
-
if (this.#socket === socket)
|
|
3974
|
-
this.#lost(event.code, event.reason);
|
|
3975
|
-
});
|
|
3976
|
-
}
|
|
3977
|
-
#send(socket, message) {
|
|
3978
|
-
if (socket.readyState !== WebSocket.OPEN)
|
|
3979
|
-
return false;
|
|
3980
|
-
try {
|
|
3981
|
-
socket.send(JSON.stringify(message));
|
|
3982
|
-
return true;
|
|
3983
|
-
} catch {
|
|
3984
|
-
return false;
|
|
3985
|
-
}
|
|
3986
|
-
}
|
|
3987
|
-
#refuseSpawn(socket, data) {
|
|
3988
|
-
if (typeof data !== "string" || data.length > 65536)
|
|
3989
|
-
return;
|
|
3990
|
-
let frame;
|
|
3991
|
-
try {
|
|
3992
|
-
frame = JSON.parse(data);
|
|
3993
|
-
} catch {
|
|
3994
|
-
return;
|
|
3995
|
-
}
|
|
3996
|
-
if (frame?.t !== "spawn" || !Number.isInteger(frame.cluster) || !Number.isInteger(frame.epoch))
|
|
3997
|
-
return;
|
|
3998
|
-
this.#send(socket, {
|
|
3999
|
-
t: "error",
|
|
4000
|
-
cluster: frame.cluster,
|
|
4001
|
-
epoch: frame.epoch,
|
|
4002
|
-
message: "This host refused the spawn request (malformed or not allowed)."
|
|
4003
|
-
});
|
|
4004
|
-
}
|
|
4005
|
-
#message(socket, data) {
|
|
4006
|
-
if (this.#socket !== socket)
|
|
4007
|
-
return;
|
|
4008
|
-
const message = parseToHost(data);
|
|
4009
|
-
if (!message) {
|
|
4010
|
-
this.#emit("error", new Error("The coordinator sent a malformed frame."));
|
|
4011
|
-
this.#refuseSpawn(socket, data);
|
|
4012
|
-
return;
|
|
4013
|
-
}
|
|
4014
|
-
this.#lastRx = Date.now();
|
|
4015
|
-
switch (message.t) {
|
|
4016
|
-
case "challenge":
|
|
4017
|
-
if (message.version !== REMOTE_PROTOCOL_VERSION) {
|
|
4018
|
-
socket.close(RemoteClose.VersionMismatch, "Protocol version mismatch.");
|
|
4019
|
-
return;
|
|
4020
|
-
}
|
|
4021
|
-
this.#send(socket, {
|
|
4022
|
-
t: "hello",
|
|
4023
|
-
id: this.#options.id,
|
|
4024
|
-
mac: signChallenge(this.#options.secret, message.nonce, this.#options.id),
|
|
4025
|
-
version: REMOTE_PROTOCOL_VERSION
|
|
4026
|
-
});
|
|
4027
|
-
return;
|
|
4028
|
-
case "welcome":
|
|
4029
|
-
this.#lease = message.leaseMs;
|
|
4030
|
-
this.#accepted = true;
|
|
4031
|
-
this.#connected = true;
|
|
4032
|
-
this.#delay = this.#options.reconnectDelay ?? 1000;
|
|
4033
|
-
this.#startWatchdog();
|
|
4034
|
-
this.#firstConnect?.resolve();
|
|
4035
|
-
this.#firstConnect = undefined;
|
|
4036
|
-
this.#emit("connected");
|
|
4037
|
-
return;
|
|
4038
|
-
case "ping":
|
|
4039
|
-
this.#send(socket, { t: "pong", n: message.n });
|
|
4040
|
-
return;
|
|
4041
|
-
case "bye":
|
|
4042
|
-
this.#fence();
|
|
4043
|
-
return;
|
|
4044
|
-
case "spawn":
|
|
4045
|
-
this.#spawn(socket, message.cluster, message.epoch, message.env);
|
|
4046
|
-
return;
|
|
4047
|
-
case "frame": {
|
|
4048
|
-
const run = this.#running.get(message.cluster);
|
|
4049
|
-
if (run?.epoch === message.epoch && run.child.connected)
|
|
4050
|
-
try {
|
|
4051
|
-
run.child.send(message.frame);
|
|
4052
|
-
} catch {}
|
|
4053
|
-
return;
|
|
4054
|
-
}
|
|
4055
|
-
case "kill": {
|
|
4056
|
-
const run = this.#running.get(message.cluster);
|
|
4057
|
-
if (run?.epoch === message.epoch && KILL_SIGNALS.has(message.signal))
|
|
4058
|
-
try {
|
|
4059
|
-
run.child.kill(message.signal);
|
|
4060
|
-
} catch {}
|
|
4061
|
-
return;
|
|
4062
|
-
}
|
|
4063
|
-
}
|
|
4064
|
-
}
|
|
4065
|
-
#spawn(socket, cluster, epoch, env) {
|
|
4066
|
-
const reply = (message) => {
|
|
4067
|
-
if (this.#socket === socket)
|
|
4068
|
-
this.#send(socket, message);
|
|
4069
|
-
};
|
|
4070
|
-
if (this.#running.has(cluster)) {
|
|
4071
|
-
reply({
|
|
4072
|
-
t: "error",
|
|
4073
|
-
cluster,
|
|
4074
|
-
epoch,
|
|
4075
|
-
message: `Cluster ${cluster} is already running here.`
|
|
4076
|
-
});
|
|
4077
|
-
return;
|
|
4078
|
-
}
|
|
4079
|
-
let child;
|
|
4080
|
-
try {
|
|
4081
|
-
const forkWorker = this.#options.fork ?? defaultFork;
|
|
4082
|
-
child = forkWorker(this.#options.script, {
|
|
4083
|
-
...process.env,
|
|
4084
|
-
...env
|
|
4085
|
-
});
|
|
4086
|
-
} catch (error) {
|
|
4087
|
-
reply({
|
|
4088
|
-
t: "error",
|
|
4089
|
-
cluster,
|
|
4090
|
-
epoch,
|
|
4091
|
-
message: error instanceof Error ? error.message : String(error)
|
|
4092
|
-
});
|
|
4093
|
-
return;
|
|
4094
|
-
}
|
|
4095
|
-
this.#running.set(cluster, { epoch, child });
|
|
4096
|
-
child.on("message", (frame) => reply({ t: "frame", cluster, epoch, frame }));
|
|
4097
|
-
child.once("exit", (code, signal) => {
|
|
4098
|
-
if (this.#running.get(cluster)?.child === child)
|
|
4099
|
-
this.#running.delete(cluster);
|
|
4100
|
-
this.#emit("clusterExit", cluster, code, signal);
|
|
4101
|
-
reply({ t: "exit", cluster, epoch, code, signal });
|
|
4102
|
-
});
|
|
4103
|
-
child.on("error", (error) => this.#emit("error", error instanceof Error ? error : new Error(String(error))));
|
|
4104
|
-
reply({
|
|
4105
|
-
t: "spawned",
|
|
4106
|
-
cluster,
|
|
4107
|
-
epoch,
|
|
4108
|
-
...child.pid ? { pid: child.pid } : {}
|
|
4109
|
-
});
|
|
4110
|
-
this.#emit("clusterSpawn", cluster);
|
|
4111
|
-
}
|
|
4112
|
-
#fence() {
|
|
4113
|
-
for (const { child } of [...this.#running.values()])
|
|
4114
|
-
try {
|
|
4115
|
-
child.kill("SIGKILL");
|
|
4116
|
-
} catch {}
|
|
4117
|
-
}
|
|
4118
|
-
#startWatchdog() {
|
|
4119
|
-
if (this.#watchdog)
|
|
4120
|
-
clearInterval(this.#watchdog);
|
|
4121
|
-
this.#lastRx = Date.now();
|
|
4122
|
-
this.#watchdog = setInterval(() => {
|
|
4123
|
-
if (Date.now() - this.#lastRx > this.#lease) {
|
|
4124
|
-
this.#fence();
|
|
4125
|
-
this.#socket?.close(RemoteClose.HostLost, "Coordinator silent.");
|
|
4126
|
-
}
|
|
4127
|
-
}, Math.max(10, Math.floor(this.#lease / 3)));
|
|
4128
|
-
this.#watchdog.unref?.();
|
|
4129
|
-
}
|
|
4130
|
-
#lost(code, reason) {
|
|
4131
|
-
if (this.#watchdog)
|
|
4132
|
-
clearInterval(this.#watchdog);
|
|
4133
|
-
this.#watchdog = undefined;
|
|
4134
|
-
this.#socket = undefined;
|
|
4135
|
-
const wasConnected = this.#connected;
|
|
4136
|
-
this.#connected = false;
|
|
4137
|
-
this.#fence();
|
|
4138
|
-
if (wasConnected)
|
|
4139
|
-
this.#emit("disconnected", { code, reason });
|
|
4140
|
-
if (this.#closed)
|
|
4141
|
-
return;
|
|
4142
|
-
const refused = code === RemoteClose.AuthFailed || code === RemoteClose.UnknownHost || code === RemoteClose.DuplicateHost || code === RemoteClose.VersionMismatch;
|
|
4143
|
-
const first = this.#firstConnect;
|
|
4144
|
-
const busy = code === RemoteClose.TooManyConnections || code === RemoteClose.HelloTimeout;
|
|
4145
|
-
const accepted = this.#accepted;
|
|
4146
|
-
if (this.#urls.length > 1)
|
|
4147
|
-
this.#urlIndex = (this.#urlIndex + 1) % this.#urls.length;
|
|
4148
|
-
if (first && !refused && !busy && !accepted && ++this.#unreachable < this.#urls.length) {
|
|
4149
|
-
this.#open();
|
|
4150
|
-
return;
|
|
4151
|
-
}
|
|
4152
|
-
if (first && busy && ++this.#busyAttempts < (this.#options.connectAttempts ?? 10)) {
|
|
4153
|
-
this.#retryLater();
|
|
4154
|
-
return;
|
|
4155
|
-
}
|
|
4156
|
-
if (first) {
|
|
4157
|
-
this.#firstConnect = undefined;
|
|
4158
|
-
this.#closed = true;
|
|
4159
|
-
first.reject(new Error(`The coordinator refused this host (${code}${reason ? `: ${reason}` : ""}).`));
|
|
4160
|
-
return;
|
|
4161
|
-
}
|
|
4162
|
-
if (refused)
|
|
4163
|
-
this.#emit("error", new Error(`The coordinator refused this host (${code}: ${reason}).`));
|
|
4164
|
-
this.#retryLater();
|
|
4165
|
-
}
|
|
4166
|
-
#retryLater() {
|
|
4167
|
-
const wait = Math.round(this.#delay * (1 + Math.random() * 0.2));
|
|
4168
|
-
this.#delay = Math.min(this.#options.maxReconnectDelay ?? 30000, this.#delay * 2);
|
|
4169
|
-
this.#retry = setTimeout(() => {
|
|
4170
|
-
this.#retry = undefined;
|
|
4171
|
-
if (!this.#closed)
|
|
4172
|
-
this.#open();
|
|
4173
|
-
}, wait);
|
|
4174
|
-
}
|
|
4175
|
-
}
|
|
4176
|
-
function defaultFork(script, env) {
|
|
4177
|
-
return fork2(script, [], { env });
|
|
4178
|
-
}
|
|
4179
|
-
// packages/sharding/src/leader.ts
|
|
4180
|
-
var ACQUIRE = `local v = redis.call('GET', KEYS[1])
|
|
4181
|
-
if v == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end
|
|
4182
|
-
if not v then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return 1 end
|
|
4183
|
-
return 0`;
|
|
4184
|
-
var RELEASE = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end
|
|
4185
|
-
return 0`;
|
|
4186
|
-
|
|
4187
|
-
class RedisLeaderLock {
|
|
4188
|
-
#client;
|
|
4189
|
-
#key;
|
|
4190
|
-
#token;
|
|
4191
|
-
constructor(options) {
|
|
4192
|
-
if (typeof options?.client?.send !== "function")
|
|
4193
|
-
throw new TypeError("RedisLeaderLock needs a client with send().");
|
|
4194
|
-
this.#client = options.client;
|
|
4195
|
-
this.#key = options.key ?? "lunibee:cluster:leader";
|
|
4196
|
-
this.#token = options.token ?? crypto.randomUUID();
|
|
4197
|
-
if (!this.#key || !this.#token)
|
|
4198
|
-
throw new TypeError("key and token must not be empty.");
|
|
4199
|
-
}
|
|
4200
|
-
async acquire(ttlMs) {
|
|
4201
|
-
if (!Number.isInteger(ttlMs) || ttlMs < 1)
|
|
4202
|
-
throw new RangeError("ttlMs must be a positive integer.");
|
|
4203
|
-
const reply = await this.#client.send("EVAL", [
|
|
4204
|
-
ACQUIRE,
|
|
4205
|
-
"1",
|
|
4206
|
-
this.#key,
|
|
4207
|
-
this.#token,
|
|
4208
|
-
String(ttlMs)
|
|
4209
|
-
]);
|
|
4210
|
-
return Number(reply) === 1;
|
|
4211
|
-
}
|
|
4212
|
-
async release() {
|
|
4213
|
-
await this.#client.send("EVAL", [RELEASE, "1", this.#key, this.#token]);
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
|
|
4217
|
-
// packages/sharding/src/index.ts
|
|
4218
|
-
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4219
|
-
function needsConnection(gateway) {
|
|
4220
|
-
return gateway.state === "CONNECT" /* Connect */ || gateway.state === "RECONNECT" /* Reconnect */ || gateway.state === "CLOSED" /* Closed */;
|
|
4221
|
-
}
|
|
4222
|
-
var HANDSHAKE_SENT = new Set([
|
|
4223
|
-
"IDENTIFY" /* Identify */,
|
|
4224
|
-
"RESUME" /* Resume */,
|
|
4225
|
-
"READY" /* Ready */,
|
|
4226
|
-
"DISPATCH" /* Dispatch */,
|
|
4227
|
-
"HEARTBEAT" /* Heartbeat */,
|
|
4228
|
-
"CLOSED" /* Closed */
|
|
4229
|
-
]);
|
|
4230
|
-
function handshakeSent(gateway, timeout) {
|
|
4231
|
-
if (HANDSHAKE_SENT.has(gateway.state))
|
|
4232
|
-
return Promise.resolve();
|
|
4233
|
-
return new Promise((resolve) => {
|
|
4234
|
-
const done = () => {
|
|
4235
|
-
clearTimeout(timer);
|
|
4236
|
-
gateway.off("stateChange", onState);
|
|
4237
|
-
resolve();
|
|
4238
|
-
};
|
|
4239
|
-
const onState = (change) => {
|
|
4240
|
-
if (HANDSHAKE_SENT.has(change.next))
|
|
4241
|
-
done();
|
|
4242
|
-
};
|
|
4243
|
-
const timer = setTimeout(done, timeout);
|
|
4244
|
-
gateway.on("stateChange", onState);
|
|
4245
|
-
});
|
|
4246
|
-
}
|
|
4247
|
-
|
|
4248
|
-
class ShardManager {
|
|
4249
|
-
static IDENTIFY_INTERVAL = 5000;
|
|
4250
|
-
shards = new Map;
|
|
4251
|
-
get shardCount() {
|
|
4252
|
-
return this.#resolved ? this.#total : 0;
|
|
4253
|
-
}
|
|
4254
|
-
get shardIds() {
|
|
4255
|
-
return [...this.shards.keys()].sort((a, b) => a - b);
|
|
4256
|
-
}
|
|
4257
|
-
#options;
|
|
4258
|
-
#ids;
|
|
4259
|
-
#gate;
|
|
4260
|
-
#worker;
|
|
4261
|
-
#lifecycle = new Map;
|
|
4262
|
-
#announced = false;
|
|
4263
|
-
#resolved = false;
|
|
4264
|
-
#destroyed = false;
|
|
4265
|
-
#auto;
|
|
4266
|
-
#fixedCount;
|
|
4267
|
-
#autoScaleTimer;
|
|
4268
|
-
#total;
|
|
4269
|
-
#listeners = new Map;
|
|
4270
|
-
#gatewayInfo;
|
|
4271
|
-
constructor(options) {
|
|
4272
|
-
if (!options.token?.trim())
|
|
4273
|
-
throw new TypeError("A Discord bot token is required.");
|
|
4274
|
-
if (!Array.isArray(options.intents))
|
|
4275
|
-
throw new TypeError("Gateway intents must be an array, e.g. [GatewayIntentBits.Guilds].");
|
|
4276
|
-
const context = options.cluster === false ? null : options.cluster ?? readClusterContext();
|
|
4277
|
-
if (context) {
|
|
4278
|
-
if (options.shardCount === "auto")
|
|
4279
|
-
throw new RangeError(`shardCount "auto" is the ClusterManager's decision inside a cluster worker.`);
|
|
4280
|
-
if (options.shardCount !== undefined && options.shardCount !== context.shardCount)
|
|
4281
|
-
throw new RangeError(`shardCount ${options.shardCount} differs from the cluster's ${context.shardCount}.`);
|
|
4282
|
-
if (options.autoScaleInterval !== undefined)
|
|
4283
|
-
throw new TypeError("autoScaleInterval belongs to ClusterManager inside a cluster worker.");
|
|
4284
|
-
if (options.shardIds && !options.shardIds.every((id) => context.shardIds.includes(id)))
|
|
4285
|
-
throw new RangeError("shardIds must be a subset of the cluster's own shards.");
|
|
4286
|
-
}
|
|
4287
|
-
const count = context ? context.shardCount : options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
|
|
4288
|
-
if (!Number.isInteger(count) || count < 1)
|
|
4289
|
-
throw new RangeError("Shard count must be a positive integer.");
|
|
4290
|
-
if (options.guildsPerShard !== undefined) {
|
|
4291
|
-
assertGuildsPerShard(options.guildsPerShard);
|
|
4292
|
-
if (options.shardCount !== "auto")
|
|
4293
|
-
throw new RangeError('guildsPerShard needs shardCount: "auto".');
|
|
4294
|
-
}
|
|
4295
|
-
this.#ids = options.shardIds ?? context?.shardIds;
|
|
4296
|
-
if (this.#ids && options.shardCount !== "auto")
|
|
4297
|
-
validateShardIds(this.#ids, count);
|
|
4298
|
-
this.#worker = options.worker === false ? undefined : options.worker ?? (context && options.cluster === undefined ? ClusterWorker.current() : undefined);
|
|
4299
|
-
this.#gate = options.identify === false ? undefined : options.identify ?? (context?.coordinatedIdentify ? this.#worker?.identifyGate : undefined);
|
|
4300
|
-
if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
|
|
4301
|
-
throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
|
|
4302
|
-
if (options.spawnDelay !== undefined && (!Number.isFinite(options.spawnDelay) || options.spawnDelay < 0))
|
|
4303
|
-
throw new RangeError("spawnDelay must be a non-negative finite number of milliseconds.");
|
|
4304
|
-
if (options.maxConcurrency !== undefined && (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency < 1))
|
|
4305
|
-
throw new RangeError("maxConcurrency must be a positive integer.");
|
|
4306
|
-
if (options.handshakeTimeout !== undefined && (!Number.isFinite(options.handshakeTimeout) || options.handshakeTimeout < 0))
|
|
4307
|
-
throw new RangeError("handshakeTimeout must be a non-negative number of milliseconds.");
|
|
4308
|
-
this.#total = count;
|
|
4309
|
-
this.#fixedCount = count;
|
|
4310
|
-
this.#auto = options.shardCount === "auto";
|
|
4311
|
-
this.#options = {
|
|
4312
|
-
spawnDelay: ShardManager.IDENTIFY_INTERVAL,
|
|
4313
|
-
...options
|
|
4314
|
-
};
|
|
4315
|
-
this.#worker?.onRequest(REQUEST_HEALTH, (data) => this.report(data?.metrics === true));
|
|
4316
|
-
this.#worker?.onRequest(REQUEST_ASSIGN, async (data) => {
|
|
4317
|
-
const ids = data?.shardIds;
|
|
4318
|
-
if (!Array.isArray(ids))
|
|
4319
|
-
throw new TypeError("shardIds must be an array.");
|
|
4320
|
-
await this.addShards(ids);
|
|
4321
|
-
return { shardIds: ids };
|
|
4322
|
-
});
|
|
4323
|
-
if (options.shardCount !== "auto")
|
|
4324
|
-
this.#initialize(count);
|
|
4325
|
-
}
|
|
4326
|
-
async fetchRecommendedShardCount() {
|
|
4327
|
-
return resolveShardCount(this.#options.token, await this.fetchGatewayInfo(), this.#options);
|
|
4328
|
-
}
|
|
4329
|
-
async fetchGatewayInfo() {
|
|
4330
|
-
const info = await fetchGatewayBot(this.#options.token);
|
|
4331
|
-
this.#gatewayInfo = info;
|
|
4332
|
-
return info;
|
|
4333
|
-
}
|
|
4334
|
-
async connect() {
|
|
4335
|
-
if (this.#autoScaleTimer) {
|
|
4336
|
-
clearInterval(this.#autoScaleTimer);
|
|
4337
|
-
this.#autoScaleTimer = undefined;
|
|
4338
|
-
}
|
|
4339
|
-
await this.#ensureInitialized();
|
|
4340
|
-
await this.#start([...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id));
|
|
4341
|
-
if (this.#options.autoScaleInterval && this.#auto && !this.#autoScaleTimer) {
|
|
4342
|
-
this.#autoScaleTimer = setInterval(() => {
|
|
4343
|
-
this.checkAutoScale();
|
|
4344
|
-
}, this.#options.autoScaleInterval);
|
|
4345
|
-
}
|
|
4346
|
-
}
|
|
4347
|
-
async addShards(shardIds) {
|
|
4348
|
-
await this.#ensureInitialized();
|
|
4349
|
-
validateShardIds(shardIds, this.#total);
|
|
4350
|
-
for (const id of shardIds)
|
|
4351
|
-
if (this.shards.has(id))
|
|
4352
|
-
throw new RangeError(`Shard ${id} is already owned.`);
|
|
4353
|
-
for (const id of shardIds)
|
|
4354
|
-
this.shards.set(id, this.#createShard(id, this.#total));
|
|
4355
|
-
if (this.#ids)
|
|
4356
|
-
this.#ids = [...this.#ids, ...shardIds];
|
|
4357
|
-
this.#announced = false;
|
|
4358
|
-
try {
|
|
4359
|
-
await this.#start([...shardIds], false);
|
|
4360
|
-
} catch (error) {
|
|
4361
|
-
for (const id of shardIds) {
|
|
4362
|
-
this.shards.get(id)?.close();
|
|
4363
|
-
this.shards.delete(id);
|
|
4364
|
-
this.#lifecycle.delete(id);
|
|
4365
|
-
}
|
|
4366
|
-
if (this.#ids)
|
|
4367
|
-
this.#ids = this.#ids.filter((id) => !shardIds.includes(id));
|
|
4368
|
-
throw error;
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
async#start(ids, destroyOnFailure = true) {
|
|
4372
|
-
const gate = this.#gate;
|
|
4373
|
-
if (gate) {
|
|
4374
|
-
const timeout = this.#options.handshakeTimeout ?? 15000;
|
|
4375
|
-
const results = await Promise.allSettled(ids.map(async (id) => {
|
|
4376
|
-
const gateway = this.shards.get(id);
|
|
4377
|
-
this.#setStatus(id, "connecting");
|
|
4378
|
-
await gate.acquire(id);
|
|
4379
|
-
try {
|
|
4380
|
-
await gateway.connect();
|
|
4381
|
-
await handshakeSent(gateway, timeout);
|
|
4382
|
-
} finally {
|
|
4383
|
-
gate.release(id);
|
|
4384
|
-
}
|
|
4385
|
-
}));
|
|
4386
|
-
const failed = results.findIndex((result) => result.status === "rejected");
|
|
4387
|
-
if (failed !== -1) {
|
|
4388
|
-
if (destroyOnFailure)
|
|
4389
|
-
this.destroy();
|
|
4390
|
-
throw new Error(`Failed to connect shard ${ids[failed]}.`, {
|
|
4391
|
-
cause: results[failed].reason
|
|
4392
|
-
});
|
|
4393
|
-
}
|
|
4394
|
-
return;
|
|
4395
|
-
}
|
|
4396
|
-
const limit = this.#gatewayInfo?.sessionStartLimit;
|
|
4397
|
-
if (limit && limit.remaining < ids.length)
|
|
4398
|
-
throw new Error(`Session start limit exhausted: ${limit.remaining} of ${limit.total} IDENTIFYs left for ${ids.length} shards; resets in ${Math.ceil(limit.resetAfter / 1000)}s.`);
|
|
4399
|
-
const concurrency = Math.min(this.#options.maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Infinity);
|
|
4400
|
-
const buckets = new Map;
|
|
4401
|
-
for (const id of ids) {
|
|
4402
|
-
const key = id % concurrency;
|
|
4403
|
-
const bucket = buckets.get(key);
|
|
4404
|
-
if (bucket)
|
|
4405
|
-
bucket.push(id);
|
|
4406
|
-
else
|
|
4407
|
-
buckets.set(key, [id]);
|
|
4408
|
-
}
|
|
4409
|
-
const rounds = Math.max(0, ...[...buckets.values()].map((bucket) => bucket.length));
|
|
4410
|
-
for (let index = 0;index < rounds; index++) {
|
|
4411
|
-
const round = [...buckets.values()].map((bucket) => bucket[index]).filter((id) => id !== undefined);
|
|
4412
|
-
for (const id of round)
|
|
4413
|
-
this.#setStatus(id, "connecting");
|
|
4414
|
-
const results = await Promise.allSettled(round.map((id) => this.shards.get(id).connect()));
|
|
4415
|
-
const failed = results.findIndex((result) => result.status === "rejected");
|
|
4416
|
-
if (limit)
|
|
4417
|
-
limit.remaining = Math.max(0, limit.remaining - results.filter((r) => r.status === "fulfilled").length);
|
|
4418
|
-
if (failed !== -1) {
|
|
4419
|
-
if (destroyOnFailure)
|
|
4420
|
-
this.destroy();
|
|
4421
|
-
throw new Error(`Failed to connect shard ${round[failed]}.`, {
|
|
4422
|
-
cause: results[failed].reason
|
|
4423
|
-
});
|
|
4424
|
-
}
|
|
4425
|
-
if (index + 1 < rounds && this.#options.spawnDelay && this.#options.spawnDelay > 0) {
|
|
4426
|
-
await Promise.all(round.map((id) => handshakeSent(this.shards.get(id), this.#options.handshakeTimeout ?? 15000)));
|
|
4427
|
-
await sleep(this.#options.spawnDelay);
|
|
4428
|
-
}
|
|
4429
|
-
}
|
|
4430
|
-
}
|
|
4431
|
-
async checkAutoScale() {
|
|
4432
|
-
if (!this.#auto)
|
|
4433
|
-
return;
|
|
4434
|
-
try {
|
|
4435
|
-
const recommended = await this.fetchRecommendedShardCount();
|
|
4436
|
-
if (recommended !== this.shardCount) {
|
|
4437
|
-
await this.reshard(recommended);
|
|
4438
|
-
}
|
|
4439
|
-
} catch (error) {
|
|
4440
|
-
this.#options.onAutoScaleError?.(error);
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
|
-
async reshard(count) {
|
|
4444
|
-
this.#options.shardCount = count;
|
|
4445
|
-
this.destroy();
|
|
4446
|
-
await this.connect();
|
|
4447
|
-
}
|
|
4448
|
-
destroy() {
|
|
4449
|
-
for (const shard of this.shards.values())
|
|
4450
|
-
shard.close();
|
|
4451
|
-
for (const id of this.shards.keys())
|
|
4452
|
-
this.#setStatus(id, "destroyed");
|
|
4453
|
-
this.shards.clear();
|
|
4454
|
-
this.#lifecycle.clear();
|
|
4455
|
-
this.#announced = false;
|
|
4456
|
-
this.#resolved = false;
|
|
4457
|
-
this.#destroyed = true;
|
|
4458
|
-
if (this.#autoScaleTimer) {
|
|
4459
|
-
clearInterval(this.#autoScaleTimer);
|
|
4460
|
-
this.#autoScaleTimer = undefined;
|
|
4461
|
-
}
|
|
4462
|
-
}
|
|
4463
|
-
on(event, listener) {
|
|
4464
|
-
let set = this.#listeners.get(event);
|
|
4465
|
-
if (!set)
|
|
4466
|
-
this.#listeners.set(event, set = new Set);
|
|
4467
|
-
set.add(listener);
|
|
4468
|
-
return this;
|
|
4469
|
-
}
|
|
4470
|
-
off(event, listener) {
|
|
4471
|
-
this.#listeners.get(event)?.delete(listener);
|
|
4472
|
-
return this;
|
|
4473
|
-
}
|
|
4474
|
-
#emit(event, ...args) {
|
|
4475
|
-
for (const listener of this.#listeners.get(event) ?? [])
|
|
4476
|
-
try {
|
|
4477
|
-
listener(...args);
|
|
4478
|
-
} catch {}
|
|
4479
|
-
}
|
|
4480
|
-
getShardIdForGuild(guildId) {
|
|
4481
|
-
const id = BigInt(guildId);
|
|
4482
|
-
return Number((id >> 22n) % BigInt(this.#total));
|
|
4483
|
-
}
|
|
4484
|
-
sendVoiceState(guildId, channelId, options = {}) {
|
|
4485
|
-
return this.shards.get(this.getShardIdForGuild(guildId))?.sendVoiceState(guildId, channelId, options) ?? false;
|
|
4486
|
-
}
|
|
4487
|
-
get(id) {
|
|
4488
|
-
return this.shards.get(id);
|
|
4489
|
-
}
|
|
4490
|
-
health() {
|
|
4491
|
-
return [...this.shards].map(([id, gateway]) => {
|
|
4492
|
-
const life = this.#lifecycle.get(id);
|
|
4493
|
-
return {
|
|
4494
|
-
id,
|
|
4495
|
-
state: gateway.state,
|
|
4496
|
-
ping: gateway.ping,
|
|
4497
|
-
status: life?.status ?? "idle",
|
|
4498
|
-
...life?.readyAt !== undefined ? { readyAt: life.readyAt } : {},
|
|
4499
|
-
...life?.disconnectedAt !== undefined ? { disconnectedAt: life.disconnectedAt } : {}
|
|
4500
|
-
};
|
|
4501
|
-
});
|
|
4502
|
-
}
|
|
4503
|
-
report(metrics = false) {
|
|
4504
|
-
let numbers;
|
|
4505
|
-
if (metrics) {
|
|
4506
|
-
const memory = process.memoryUsage();
|
|
4507
|
-
const cpu = process.cpuUsage();
|
|
4508
|
-
numbers = {
|
|
4509
|
-
rssBytes: memory.rss,
|
|
4510
|
-
heapUsedBytes: memory.heapUsed,
|
|
4511
|
-
cpuUserMicros: cpu.user,
|
|
4512
|
-
cpuSystemMicros: cpu.system
|
|
4513
|
-
};
|
|
4514
|
-
}
|
|
4515
|
-
return {
|
|
4516
|
-
shards: this.health(),
|
|
4517
|
-
...numbers ? { metrics: numbers } : {},
|
|
4518
|
-
reportedAt: Date.now()
|
|
4519
|
-
};
|
|
4520
|
-
}
|
|
4521
|
-
#setStatus(id, status) {
|
|
4522
|
-
const life = this.#lifecycle.get(id) ?? { status };
|
|
4523
|
-
life.status = status;
|
|
4524
|
-
const now = Date.now();
|
|
4525
|
-
if (status === "ready")
|
|
4526
|
-
life.readyAt = now;
|
|
4527
|
-
if (status === "disconnected")
|
|
4528
|
-
life.disconnectedAt = now;
|
|
4529
|
-
this.#lifecycle.set(id, life);
|
|
4530
|
-
if (status !== "ready")
|
|
4531
|
-
this.#announced = false;
|
|
4532
|
-
else
|
|
4533
|
-
this.#maybeAnnounce();
|
|
4534
|
-
}
|
|
4535
|
-
#maybeAnnounce() {
|
|
4536
|
-
if (this.#announced || this.shards.size === 0)
|
|
4537
|
-
return;
|
|
4538
|
-
for (const id of this.shards.keys())
|
|
4539
|
-
if (this.#lifecycle.get(id)?.status !== "ready")
|
|
4540
|
-
return;
|
|
4541
|
-
this.#announced = true;
|
|
4542
|
-
this.#emit("ready");
|
|
4543
|
-
this.#worker?.ready(this.shardIds);
|
|
4544
|
-
this.#worker?.reportHealth(this.report());
|
|
4545
|
-
}
|
|
4546
|
-
values() {
|
|
4547
|
-
return [...this.shards].map(([id, gateway]) => ({ id, gateway }));
|
|
4548
|
-
}
|
|
4549
|
-
async#ensureInitialized() {
|
|
4550
|
-
if (this.#resolved)
|
|
4551
|
-
return;
|
|
4552
|
-
if (this.#destroyed)
|
|
4553
|
-
this.#destroyed = false;
|
|
4554
|
-
const count = this.#options.shardCount === "auto" ? await this.fetchRecommendedShardCount() : this.#fixedCount;
|
|
4555
|
-
this.#initialize(count);
|
|
4556
|
-
}
|
|
4557
|
-
#initialize(count) {
|
|
4558
|
-
if (this.#resolved)
|
|
4559
|
-
return;
|
|
4560
|
-
this.#total = count;
|
|
4561
|
-
const ids = this.#ids ?? Array.from({ length: count }, (_, i) => i);
|
|
4562
|
-
validateShardIds(ids, count);
|
|
4563
|
-
for (const id of ids)
|
|
4564
|
-
this.shards.set(id, this.#createShard(id, count));
|
|
4565
|
-
this.#resolved = true;
|
|
4566
|
-
}
|
|
4567
|
-
#createShard(id, count) {
|
|
4568
|
-
const gateway = new Gateway({
|
|
4569
|
-
token: this.#options.token,
|
|
4570
|
-
intents: this.#options.intents,
|
|
4571
|
-
shardId: id,
|
|
4572
|
-
shardCount: count,
|
|
4573
|
-
reconnect: this.#options.reconnect ?? true
|
|
4574
|
-
});
|
|
4575
|
-
gateway.on("ready", () => {
|
|
4576
|
-
this.#setStatus(id, "ready");
|
|
4577
|
-
this.#emit("shardReady", id);
|
|
4578
|
-
});
|
|
4579
|
-
gateway.on("resumed", () => {
|
|
4580
|
-
this.#setStatus(id, "ready");
|
|
4581
|
-
this.#emit("shardResume", id);
|
|
4582
|
-
});
|
|
4583
|
-
gateway.on("close", (close) => {
|
|
4584
|
-
this.#setStatus(id, "disconnected");
|
|
4585
|
-
this.#emit("shardDisconnect", id, close);
|
|
4586
|
-
});
|
|
4587
|
-
gateway.on("error", (error) => this.#emit("shardError", id, error));
|
|
4588
|
-
gateway.on("unstable", (info) => {
|
|
4589
|
-
this.#setStatus(id, "unstable");
|
|
4590
|
-
this.#emit("shardUnstable", id, info);
|
|
4591
|
-
});
|
|
4592
|
-
this.#emit("shardCreate", id, gateway);
|
|
4593
|
-
return gateway;
|
|
4594
|
-
}
|
|
4595
|
-
}
|
|
5
|
+
fetchGatewayBot2,
|
|
6
|
+
MAX_GUILDS_PER_SHARD2,
|
|
7
|
+
shardsForGuilds2,
|
|
8
|
+
fetchGuildCount2,
|
|
9
|
+
isClusterWorker2,
|
|
10
|
+
validateShardIds2,
|
|
11
|
+
allocateShards2,
|
|
12
|
+
planClusters2,
|
|
13
|
+
assertCompleteAllocation2,
|
|
14
|
+
readClusterContext2,
|
|
15
|
+
threadFork2,
|
|
16
|
+
threadEndpoint2,
|
|
17
|
+
ClusterWorker2,
|
|
18
|
+
BroadcastChannelTransport2,
|
|
19
|
+
IpcTransport2,
|
|
20
|
+
ShardBus2,
|
|
21
|
+
ShardSupervisor2,
|
|
22
|
+
IDENTIFY_INTERVAL2,
|
|
23
|
+
IdentifyCoordinator2,
|
|
24
|
+
REMOTE_PROTOCOL_VERSION2,
|
|
25
|
+
RemoteClose2,
|
|
26
|
+
RequestTracker2,
|
|
27
|
+
ClusterManager2,
|
|
28
|
+
ClusterHost2,
|
|
29
|
+
RedisLeaderLock2,
|
|
30
|
+
SEQUENTIAL_READY_TIMEOUT2,
|
|
31
|
+
connectInQueue2,
|
|
32
|
+
ShardManager2
|
|
33
|
+
} from "../chunks/index-mgw2yvrm.js";
|
|
4596
34
|
export {
|
|
4597
|
-
BroadcastChannelTransport,
|
|
4598
|
-
ClusterHost,
|
|
4599
|
-
ClusterManager,
|
|
4600
|
-
ClusterWorker,
|
|
4601
|
-
IDENTIFY_INTERVAL,
|
|
4602
|
-
IdentifyCoordinator,
|
|
4603
|
-
IpcTransport,
|
|
4604
|
-
MAX_GUILDS_PER_SHARD,
|
|
4605
|
-
REMOTE_PROTOCOL_VERSION,
|
|
4606
|
-
RedisLeaderLock,
|
|
4607
|
-
RemoteClose,
|
|
4608
|
-
RequestTracker,
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
allocateShards,
|
|
4614
|
-
assertCompleteAllocation,
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
35
|
+
BroadcastChannelTransport2 as BroadcastChannelTransport,
|
|
36
|
+
ClusterHost2 as ClusterHost,
|
|
37
|
+
ClusterManager2 as ClusterManager,
|
|
38
|
+
ClusterWorker2 as ClusterWorker,
|
|
39
|
+
IDENTIFY_INTERVAL2 as IDENTIFY_INTERVAL,
|
|
40
|
+
IdentifyCoordinator2 as IdentifyCoordinator,
|
|
41
|
+
IpcTransport2 as IpcTransport,
|
|
42
|
+
MAX_GUILDS_PER_SHARD2 as MAX_GUILDS_PER_SHARD,
|
|
43
|
+
REMOTE_PROTOCOL_VERSION2 as REMOTE_PROTOCOL_VERSION,
|
|
44
|
+
RedisLeaderLock2 as RedisLeaderLock,
|
|
45
|
+
RemoteClose2 as RemoteClose,
|
|
46
|
+
RequestTracker2 as RequestTracker,
|
|
47
|
+
SEQUENTIAL_READY_TIMEOUT2 as SEQUENTIAL_READY_TIMEOUT,
|
|
48
|
+
ShardBus2 as ShardBus,
|
|
49
|
+
ShardManager2 as ShardManager,
|
|
50
|
+
ShardSupervisor2 as ShardSupervisor,
|
|
51
|
+
allocateShards2 as allocateShards,
|
|
52
|
+
assertCompleteAllocation2 as assertCompleteAllocation,
|
|
53
|
+
connectInQueue2 as connectInQueue,
|
|
54
|
+
fetchGatewayBot2 as fetchGatewayBot,
|
|
55
|
+
fetchGuildCount2 as fetchGuildCount,
|
|
56
|
+
isClusterWorker2 as isClusterWorker,
|
|
57
|
+
planClusters2 as planClusters,
|
|
58
|
+
readClusterContext2 as readClusterContext,
|
|
59
|
+
shardsForGuilds2 as shardsForGuilds,
|
|
60
|
+
threadEndpoint2 as threadEndpoint,
|
|
61
|
+
threadFork2 as threadFork,
|
|
62
|
+
validateShardIds2 as validateShardIds
|
|
4623
63
|
};
|
|
4624
64
|
|
|
4625
|
-
//# debugId=
|
|
65
|
+
//# debugId=EA867F8F0564ED9364756E2164756E21
|