@wolfstar/plugin-sharder 0.0.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.
@@ -0,0 +1,3491 @@
1
+ import { Result, Result as Result$1 } from "@sapphire/result";
2
+ import { EventEmitter } from "node:events";
3
+ import { Worker, isMainThread, parentPort, threadId, workerData } from "node:worker_threads";
4
+ import { deserialize, serialize } from "node:v8";
5
+ import { promisify } from "node:util";
6
+ import { brotliCompress, brotliDecompress, gunzip, gzip } from "node:zlib";
7
+ import { availableParallelism, hostname } from "node:os";
8
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
9
+ import cluster from "node:cluster";
10
+ import { fileURLToPath } from "node:url";
11
+ import { fork } from "node:child_process";
12
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
13
+ import { connect, createServer } from "node:net";
14
+ import { connect as connect$1, createServer as createServer$1 } from "node:tls";
15
+
16
+ //#region src/ShardPing.ts
17
+ /**
18
+ * Pings a shard, measures its latency, and notices when it stops answering.
19
+ */
20
+ var ShardPing = class {
21
+ /**
22
+ * When the last ping was sent, `-1` before the first one.
23
+ */
24
+ lastSentTimestamp = -1;
25
+ /**
26
+ * When the last answer came, `-1` before the first one.
27
+ */
28
+ lastReceivedTimestamp = -1;
29
+ /**
30
+ * The round trip of the last ping, in milliseconds; `-1` before the first answer.
31
+ */
32
+ latency = -1;
33
+ interval;
34
+ timeout;
35
+ delaySinceReceived;
36
+ #send;
37
+ #onTimeout;
38
+ #timer = null;
39
+ #watchdog = null;
40
+ /**
41
+ * @internal
42
+ */
43
+ constructor(options, send, onTimeout) {
44
+ this.interval = options.interval ?? 45e3;
45
+ this.timeout = options.timeout ?? 6e4;
46
+ this.delaySinceReceived = options.delaySinceReceived ?? false;
47
+ this.#send = send;
48
+ this.#onTimeout = onTimeout;
49
+ }
50
+ /**
51
+ * Whether the pings are running.
52
+ */
53
+ get running() {
54
+ return this.#watchdog !== null;
55
+ }
56
+ /**
57
+ * Whether the last ping was answered.
58
+ */
59
+ get hasReceivedResponse() {
60
+ return this.lastReceivedTimestamp >= this.lastSentTimestamp;
61
+ }
62
+ get lastSentAt() {
63
+ return this.lastSentTimestamp === -1 ? null : new Date(this.lastSentTimestamp);
64
+ }
65
+ get lastReceivedAt() {
66
+ return this.lastReceivedTimestamp === -1 ? null : new Date(this.lastReceivedTimestamp);
67
+ }
68
+ /**
69
+ * When the next ping is due, or `null` when the pings are not running.
70
+ */
71
+ get nextPingTimestamp() {
72
+ if (!this.running || !this.enabled) return null;
73
+ const from = this.delaySinceReceived ? this.lastReceivedTimestamp : this.lastSentTimestamp;
74
+ return (from === -1 ? Date.now() : from) + this.interval;
75
+ }
76
+ /**
77
+ * @internal
78
+ */
79
+ start() {
80
+ if (this.running || !this.enabled) return;
81
+ this.#watchdog = setTimeout(() => {
82
+ this.stop();
83
+ this.#onTimeout();
84
+ }, this.timeout);
85
+ this.#watchdog.unref();
86
+ this.#schedule();
87
+ }
88
+ /**
89
+ * @internal
90
+ */
91
+ stop() {
92
+ if (this.#timer) clearTimeout(this.#timer);
93
+ if (this.#watchdog) clearTimeout(this.#watchdog);
94
+ this.#timer = null;
95
+ this.#watchdog = null;
96
+ }
97
+ /**
98
+ * @internal
99
+ */
100
+ receive(sentAt) {
101
+ this.lastReceivedTimestamp = Date.now();
102
+ this.latency = this.lastReceivedTimestamp - sentAt;
103
+ this.#watchdog?.refresh();
104
+ if (this.delaySinceReceived && this.running) this.#schedule();
105
+ }
106
+ /**
107
+ * Whether the manager pings at all.
108
+ */
109
+ get enabled() {
110
+ return this.interval !== -1 && this.interval !== Number.POSITIVE_INFINITY;
111
+ }
112
+ #schedule() {
113
+ if (!this.enabled) return;
114
+ if (this.#timer) clearTimeout(this.#timer);
115
+ this.#timer = setTimeout(() => {
116
+ this.#timer = null;
117
+ this.lastSentTimestamp = Date.now();
118
+ this.#send(this.lastSentTimestamp).catch(() => void 0);
119
+ if (!this.delaySinceReceived) this.#schedule();
120
+ }, this.interval);
121
+ this.#timer.unref();
122
+ }
123
+ };
124
+
125
+ //#endregion
126
+ //#region src/messages/protocol.ts
127
+ /**
128
+ * The lifecycle status of a shard, as it signals it to its manager.
129
+ */
130
+ const ShardStatus = {
131
+ /**
132
+ * Not spawned yet, or stopped.
133
+ */
134
+ Idle: "Idle",
135
+ /**
136
+ * Spawned, and not ready to answer yet.
137
+ */
138
+ Starting: "Starting",
139
+ /**
140
+ * Fully operative.
141
+ */
142
+ Ready: "Ready",
143
+ /**
144
+ * Running, but disconnected from what it serves, e.g. the gateway.
145
+ */
146
+ Disconnected: "Disconnected",
147
+ /**
148
+ * Running, and reconnecting to what it serves.
149
+ */
150
+ Reconnecting: "Reconnecting",
151
+ /**
152
+ * Shutting down, not to be restarted.
153
+ */
154
+ Exiting: "Exiting",
155
+ /**
156
+ * Shutting down, to be restarted.
157
+ */
158
+ Restarting: "Restarting"
159
+ };
160
+ /**
161
+ * The operation of a packet.
162
+ *
163
+ * @internal
164
+ */
165
+ const Op = {
166
+ Signal: 0,
167
+ Ping: 1,
168
+ Pong: 2,
169
+ Message: 3,
170
+ Request: 4,
171
+ Reply: 5,
172
+ Abort: 6,
173
+ Close: 7
174
+ };
175
+ const Ops = new Set(Object.values(Op));
176
+ /**
177
+ * Serializes packets with a {@link MessageHandler}, then runs them through the {@link MessageTransformer}s, and the
178
+ * other way around.
179
+ *
180
+ * @internal
181
+ */
182
+ var PacketCodec = class {
183
+ handler;
184
+ transformers;
185
+ constructor(handler, transformers) {
186
+ this.handler = handler;
187
+ this.transformers = transformers;
188
+ }
189
+ async encode(packet, context) {
190
+ let data = this.handler.serialize(packet);
191
+ for (const transformer of this.transformers) data = await transformer.write(assertChannelData(data), context);
192
+ return data;
193
+ }
194
+ async decode(data, context) {
195
+ for (let index = this.transformers.length - 1; index >= 0; --index) data = await this.transformers[index].read(assertChannelData(data), context);
196
+ const packet = this.handler.deserialize(data);
197
+ if (!isPacket(packet)) throw new TypeError("The message is not a sharder packet");
198
+ return packet;
199
+ }
200
+ };
201
+ function assertChannelData(data) {
202
+ if (typeof data === "string" || data instanceof Uint8Array) return data;
203
+ throw new TypeError("Transformers need a message handler serializing to strings or bytes");
204
+ }
205
+ function isPacket(value) {
206
+ return typeof value === "object" && value !== null && Ops.has(value.op);
207
+ }
208
+ /**
209
+ * Serializes an error to send it over the channel.
210
+ *
211
+ * @internal
212
+ */
213
+ function serializeError(error) {
214
+ return error instanceof Error ? {
215
+ name: error.name,
216
+ message: error.message
217
+ } : {
218
+ name: "Error",
219
+ message: String(error)
220
+ };
221
+ }
222
+
223
+ //#endregion
224
+ //#region src/util/errors.ts
225
+ /**
226
+ * The handler of a request threw, or there was none: the error is the remote one, rebuilt.
227
+ */
228
+ var ShardRequestError = class extends Error {
229
+ /**
230
+ * The name of the remote error, e.g. `TypeError`.
231
+ */
232
+ remoteName;
233
+ constructor(remoteName, message) {
234
+ super(message);
235
+ this.name = "ShardRequestError";
236
+ this.remoteName = remoteName;
237
+ }
238
+ };
239
+ /**
240
+ * A request got no reply in time. The other side was told to abort it.
241
+ */
242
+ var ShardRequestTimeoutError = class extends Error {
243
+ timeout;
244
+ constructor(timeout) {
245
+ super(`The request got no reply within ${timeout}ms`);
246
+ this.name = "ShardRequestTimeoutError";
247
+ this.timeout = timeout;
248
+ }
249
+ };
250
+ /**
251
+ * A shard stopped, is not ready in time, or does not exist.
252
+ */
253
+ var ShardUnavailableError = class extends Error {
254
+ channelId;
255
+ constructor(channelId, reason) {
256
+ super(`Shard ${channelId} is unavailable: ${reason}`);
257
+ this.name = "ShardUnavailableError";
258
+ this.channelId = channelId;
259
+ }
260
+ };
261
+ /**
262
+ * A shard failed before it was ready: its process exited, or its worker could not start. The sharder RFC's `error`
263
+ * signal.
264
+ */
265
+ var ShardSpawnError = class extends Error {
266
+ channelId;
267
+ /**
268
+ * The exit code, when the shard exited.
269
+ */
270
+ code;
271
+ constructor(channelId, code, cause) {
272
+ super(cause instanceof Error ? `Shard ${channelId} failed to start: ${cause.message}` : `Shard ${channelId} exited with code ${code} before it was ready`, { cause });
273
+ this.name = "ShardSpawnError";
274
+ this.channelId = channelId;
275
+ this.code = code;
276
+ }
277
+ };
278
+
279
+ //#endregion
280
+ //#region src/util/requests.ts
281
+ /**
282
+ * Tracks the requests sent on a channel until their reply comes, they time out, or they are aborted.
283
+ *
284
+ * @internal
285
+ */
286
+ var OutgoingRequests = class {
287
+ #pending = /* @__PURE__ */ new Map();
288
+ #step;
289
+ #nonce = 0;
290
+ /**
291
+ * @param negative Whether to count nonces down from -1, so two senders on one channel never collide.
292
+ */
293
+ constructor(negative = false) {
294
+ this.#step = negative ? -1 : 1;
295
+ }
296
+ /**
297
+ * Sends a request.
298
+ *
299
+ * @param send Writes the request.
300
+ * @param fields The request.
301
+ * @param timeout How long to wait for the reply.
302
+ * @param signal Aborts the request.
303
+ * @param owner What the request was sent to, to reject it with {@link OutgoingRequests.rejectOwner}.
304
+ */
305
+ request(send, fields, timeout, signal, owner) {
306
+ if (signal?.aborted) return Promise.reject(signal.reason);
307
+ this.#nonce += this.#step;
308
+ const nonce = this.#nonce;
309
+ return new Promise((resolve, reject) => {
310
+ const abort = (error) => {
311
+ if (!this.#pending.has(nonce)) return;
312
+ settle();
313
+ reject(error);
314
+ send({
315
+ op: Op.Abort,
316
+ nonce
317
+ }).catch(() => void 0);
318
+ };
319
+ const timer = setTimeout(() => abort(new ShardRequestTimeoutError(timeout)), timeout);
320
+ const onAbort = () => abort(signal.reason);
321
+ signal?.addEventListener("abort", onAbort, { once: true });
322
+ const settle = () => {
323
+ clearTimeout(timer);
324
+ signal?.removeEventListener("abort", onAbort);
325
+ this.#pending.delete(nonce);
326
+ };
327
+ this.#pending.set(nonce, {
328
+ owner,
329
+ resolve: (value) => {
330
+ settle();
331
+ resolve(value);
332
+ },
333
+ reject: (error) => {
334
+ settle();
335
+ reject(error);
336
+ }
337
+ });
338
+ send({
339
+ op: Op.Request,
340
+ nonce,
341
+ ...fields,
342
+ timeout
343
+ }).catch((error) => {
344
+ this.#pending.get(nonce)?.reject(error);
345
+ });
346
+ });
347
+ }
348
+ /**
349
+ * Settles the request a reply answers. Returns whether it answered one of these requests.
350
+ */
351
+ settle(packet) {
352
+ const pending = this.#pending.get(packet.nonce);
353
+ if (!pending) return false;
354
+ if (packet.error) pending.reject(new ShardRequestError(packet.error.name, packet.error.message));
355
+ else pending.resolve(packet.body);
356
+ return true;
357
+ }
358
+ rejectOwner(owner, error) {
359
+ for (const pending of this.#pending.values()) if (pending.owner === owner) pending.reject(error);
360
+ }
361
+ rejectAll(error) {
362
+ for (const pending of this.#pending.values()) pending.reject(error);
363
+ }
364
+ };
365
+ /**
366
+ * Runs the requests received on a channel, and aborts them when the other side does.
367
+ *
368
+ * @internal
369
+ */
370
+ var IncomingRequests = class {
371
+ #controllers = /* @__PURE__ */ new Map();
372
+ async handle(send, packet, handler, context) {
373
+ const controller = new AbortController();
374
+ this.#controllers.set(packet.nonce, controller);
375
+ let reply;
376
+ try {
377
+ if (!handler) throw new Error("There is no request handler");
378
+ let body = await handler(packet.body, {
379
+ ...context,
380
+ signal: controller.signal
381
+ });
382
+ if (Result$1.isResult(body)) {
383
+ if (body.isErr()) throw body.unwrapErr();
384
+ body = body.unwrap();
385
+ }
386
+ reply = {
387
+ op: Op.Reply,
388
+ nonce: packet.nonce,
389
+ body
390
+ };
391
+ } catch (error) {
392
+ reply = {
393
+ op: Op.Reply,
394
+ nonce: packet.nonce,
395
+ error: serializeError(error)
396
+ };
397
+ } finally {
398
+ this.#controllers.delete(packet.nonce);
399
+ }
400
+ if (!controller.signal.aborted) await send(reply).catch(() => void 0);
401
+ }
402
+ abort(nonce) {
403
+ this.#controllers.get(nonce)?.abort(/* @__PURE__ */ new Error("The request was aborted by its sender"));
404
+ }
405
+ abortAll() {
406
+ for (const nonce of this.#controllers.keys()) this.abort(nonce);
407
+ }
408
+ };
409
+ /**
410
+ * Turns an operation into a `Result`, for the `try*` methods.
411
+ *
412
+ * @internal
413
+ */
414
+ async function toResult(operation) {
415
+ return Result$1.fromAsync(operation);
416
+ }
417
+ /**
418
+ * Turns the outcomes of a partial broadcast into `Result`s.
419
+ *
420
+ * @internal
421
+ */
422
+ function settledToResults(results) {
423
+ return results.map((result) => result.status === "fulfilled" ? Result$1.ok(result.value) : Result$1.err(result.reason));
424
+ }
425
+ /**
426
+ * Serializes the outcomes of a partial broadcast.
427
+ *
428
+ * @internal
429
+ */
430
+ function serializeSettled(results) {
431
+ return results.map((result) => result.status === "fulfilled" ? {
432
+ status: "fulfilled",
433
+ value: result.value
434
+ } : {
435
+ status: "rejected",
436
+ reason: serializeError(result.reason)
437
+ });
438
+ }
439
+ /**
440
+ * Rebuilds the outcomes of a partial broadcast.
441
+ *
442
+ * @internal
443
+ */
444
+ function deserializeSettled(results) {
445
+ return results.map((result) => result.status === "fulfilled" ? {
446
+ status: "fulfilled",
447
+ value: result.value
448
+ } : {
449
+ status: "rejected",
450
+ reason: new ShardRequestError(result.reason.name, result.reason.message)
451
+ });
452
+ }
453
+
454
+ //#endregion
455
+ //#region src/ShardChannel.ts
456
+ /**
457
+ * The manager's channel to one shard: a process, cluster worker, worker thread, or remote process, connecting some
458
+ * gateway shards.
459
+ *
460
+ * @remarks
461
+ * Named after discord.js's sharder: a "shard" is the client a manager spawns, and the channel is how the manager
462
+ * talks to it, whatever the channel strategy.
463
+ */
464
+ var ShardChannel = class extends EventEmitter {
465
+ /**
466
+ * The ID of the channel, its index in {@link ShardManager.channels}.
467
+ */
468
+ id;
469
+ /**
470
+ * The IDs of the gateway shards the shard connects.
471
+ */
472
+ shards;
473
+ /**
474
+ * The total number of gateway shards the shard is told about.
475
+ */
476
+ shardCount;
477
+ manager;
478
+ /**
479
+ * The pings of the shard.
480
+ */
481
+ ping;
482
+ /**
483
+ * The status of the running shard, `Idle` when none runs.
484
+ */
485
+ status = ShardStatus.Idle;
486
+ #active = null;
487
+ #replacement = null;
488
+ #stopped = false;
489
+ #waiters = /* @__PURE__ */ new Set();
490
+ #outgoing = new OutgoingRequests();
491
+ /**
492
+ * @internal
493
+ */
494
+ constructor(manager, id, shards, shardCount) {
495
+ super();
496
+ this.manager = manager;
497
+ this.id = id;
498
+ this.shards = shards;
499
+ this.shardCount = shardCount;
500
+ this.ping = new ShardPing(manager.pingOptions, (sentAt) => this.#write(this.#active, {
501
+ op: Op.Ping,
502
+ sentAt
503
+ }), () => this.#unresponsive());
504
+ }
505
+ /**
506
+ * Whether the shard is running and signalled that it is ready.
507
+ */
508
+ get ready() {
509
+ return this.status === ShardStatus.Ready;
510
+ }
511
+ /**
512
+ * Whether a shard is running: spawned, and not stopped yet.
513
+ */
514
+ get running() {
515
+ return this.#active !== null;
516
+ }
517
+ /**
518
+ * Whether the channel was stopped for good: closed, exited, or given up on.
519
+ */
520
+ get stopped() {
521
+ return this.#stopped;
522
+ }
523
+ /**
524
+ * The ID of the shard's process.
525
+ */
526
+ get pid() {
527
+ return this.#active?.transport?.pid ?? null;
528
+ }
529
+ /**
530
+ * The ID of the shard's worker thread, for {@link WorkerStrategy}.
531
+ */
532
+ get threadId() {
533
+ return this.#active?.transport?.threadId ?? null;
534
+ }
535
+ /**
536
+ * The host running the shard, for strategies spawning shards elsewhere.
537
+ */
538
+ get host() {
539
+ return this.#active?.transport?.host ?? null;
540
+ }
541
+ /**
542
+ * When the running shard was spawned.
543
+ */
544
+ get startedTimestamp() {
545
+ return this.#active?.startedAt ?? null;
546
+ }
547
+ /**
548
+ * Spawns the shard, and waits for it to signal that it is ready.
549
+ *
550
+ * @param timeout How long to wait for it, in milliseconds. Past it, the shard is killed and the promise rejects.
551
+ */
552
+ async start(timeout = this.manager.spawnTimeout) {
553
+ if (this.#active) throw new Error(`Shard ${this.id} is already running`);
554
+ this.#stopped = false;
555
+ const instance = this.#spawn();
556
+ this.#active = instance;
557
+ this.#setStatus(ShardStatus.Starting);
558
+ await this.#awaitReady(instance, timeout);
559
+ }
560
+ /**
561
+ * Stops the shard for good. It is first asked to close, which runs its close handler, and is killed if it did not
562
+ * exit within the timeout.
563
+ *
564
+ * @param timeout How long to wait for the shard to exit by itself, in milliseconds.
565
+ */
566
+ async close(timeout = this.manager.requestTimeout) {
567
+ this.#stopped = true;
568
+ this.#rejectWaiters(new ShardUnavailableError(this.id, "it was closed"));
569
+ const instances = [this.#replacement, this.#active].filter((instance) => instance !== null);
570
+ if (instances.length === 0) return;
571
+ await Promise.all(instances.map((instance) => this.#stop(instance, timeout)));
572
+ this.emit("destroy");
573
+ this.manager.emit("shardDestroy", this);
574
+ }
575
+ /**
576
+ * Restarts the shard through its manager, see {@link ShardManager.restart}.
577
+ *
578
+ * @param options Whether to restart it rolling, and the timeout.
579
+ */
580
+ restart(options) {
581
+ return this.manager.restart(this.id, options);
582
+ }
583
+ /**
584
+ * Waits for the shard to be ready.
585
+ *
586
+ * @param timeout How long to wait, in milliseconds.
587
+ * @param signal Aborts the wait.
588
+ * @throws A {@link ShardUnavailableError} when the shard is stopped for good, not ready in time, or expected to be
589
+ * ready past the timeout (see `spawn.readyHint`).
590
+ */
591
+ waitForReady(timeout = this.manager.requestTimeout, signal) {
592
+ if (this.ready) return Promise.resolve();
593
+ if (this.#stopped) return Promise.reject(new ShardUnavailableError(this.id, "it is stopped"));
594
+ signal?.throwIfAborted();
595
+ const remaining = this.#estimatedRemaining();
596
+ if (remaining !== null && remaining > timeout) return Promise.reject(new ShardUnavailableError(this.id, `it is expected to be ready in ${remaining}ms`));
597
+ return new Promise((resolve, reject) => {
598
+ const settle = () => {
599
+ clearTimeout(timer);
600
+ signal?.removeEventListener("abort", onAbort);
601
+ this.#waiters.delete(waiter);
602
+ };
603
+ const waiter = {
604
+ resolve: () => {
605
+ settle();
606
+ resolve();
607
+ },
608
+ reject: (error) => {
609
+ settle();
610
+ reject(error);
611
+ }
612
+ };
613
+ const onAbort = () => waiter.reject(signal.reason);
614
+ const timer = setTimeout(() => waiter.reject(new ShardUnavailableError(this.id, `not ready within ${timeout}ms`)), timeout);
615
+ signal?.addEventListener("abort", onAbort, { once: true });
616
+ this.#waiters.add(waiter);
617
+ });
618
+ }
619
+ /**
620
+ * Sends a message to the shard, emitted as `message` by its {@link ShardClient}. Waits for the shard to be ready.
621
+ *
622
+ * @param body The message.
623
+ * @param options How long to wait for the shard to be ready, and an abort signal.
624
+ * @param from The shard the message comes from, `null` for the manager.
625
+ */
626
+ async send(body, options = {}, from = null) {
627
+ await this.waitForReady(options.timeout, options.signal);
628
+ await this.#write(this.#active, {
629
+ op: Op.Message,
630
+ body,
631
+ from
632
+ });
633
+ }
634
+ /**
635
+ * Sends a request to the shard, answered by its {@link ShardClient}'s request handler. Waits for the shard to be
636
+ * ready.
637
+ *
638
+ * @param body The request.
639
+ * @param options The timeout and abort signal of the request.
640
+ * @param from The shard the request comes from, `null` for the manager.
641
+ */
642
+ async request(body, options = {}, from = null) {
643
+ return this.#request(body, options, { from });
644
+ }
645
+ /**
646
+ * {@link ShardChannel.send}, resolving with a `Result` rather than rejecting.
647
+ */
648
+ trySend(body, options) {
649
+ return toResult(() => this.send(body, options));
650
+ }
651
+ /**
652
+ * {@link ShardChannel.request}, resolving with a `Result` rather than rejecting.
653
+ */
654
+ tryRequest(body, options) {
655
+ return toResult(() => this.request(body, options));
656
+ }
657
+ /**
658
+ * Asks the shard to start one of its gateway shards, through its shard handler.
659
+ *
660
+ * @param shardId The ID of the gateway shard.
661
+ * @param options The timeout and abort signal of the request.
662
+ */
663
+ async startShard(shardId, options) {
664
+ await this.#system("startShard", shardId, options);
665
+ }
666
+ /**
667
+ * Asks the shard to close one of its gateway shards, through its shard handler.
668
+ *
669
+ * @param shardId The ID of the gateway shard.
670
+ * @param options The timeout and abort signal of the request.
671
+ */
672
+ async closeShard(shardId, options) {
673
+ await this.#system("closeShard", shardId, options);
674
+ }
675
+ /**
676
+ * Restarts the shard, keeping the old one until the new one is ready.
677
+ *
678
+ * @internal
679
+ */
680
+ async rollingRestart(timeout = this.manager.spawnTimeout) {
681
+ if (!this.#active) {
682
+ await this.start(timeout);
683
+ return;
684
+ }
685
+ this.#stopped = false;
686
+ const replacement = this.#spawn();
687
+ this.#replacement = replacement;
688
+ try {
689
+ await this.#awaitReady(replacement, timeout);
690
+ } finally {
691
+ if (this.#replacement === replacement) this.#replacement = null;
692
+ }
693
+ const previous = this.#active;
694
+ this.#active = replacement;
695
+ this.ping.stop();
696
+ this.status = ShardStatus.Starting;
697
+ this.#setStatus(replacement.status);
698
+ if (previous) await this.#stop(previous, timeout);
699
+ }
700
+ /**
701
+ * Stops the channel for good without closing anything, when its shard exited or was given up on.
702
+ *
703
+ * @internal
704
+ */
705
+ markStopped(reason) {
706
+ this.#stopped = true;
707
+ this.#rejectWaiters(new ShardUnavailableError(this.id, reason));
708
+ }
709
+ async #request(body, options, fields) {
710
+ const timeout = options.timeout ?? this.manager.requestTimeout;
711
+ const started = Date.now();
712
+ await this.waitForReady(timeout, options.signal);
713
+ const active = this.#active;
714
+ return this.#outgoing.request((packet) => this.#write(active, packet), {
715
+ body,
716
+ ...fields
717
+ }, Math.max(timeout - (Date.now() - started), 1), options.signal, active);
718
+ }
719
+ #system(call, body, options = {}) {
720
+ return this.#request(body, options, { system: call });
721
+ }
722
+ #spawn() {
723
+ let resolveExit;
724
+ let resolveReady;
725
+ const instance = {
726
+ transport: null,
727
+ status: ShardStatus.Starting,
728
+ startedAt: Date.now(),
729
+ everReady: false,
730
+ exited: false,
731
+ exit: new Promise((resolve) => {
732
+ resolveExit = resolve;
733
+ }),
734
+ resolveExit: (code) => resolveExit(code),
735
+ ready: new Promise((resolve) => {
736
+ resolveReady = resolve;
737
+ }),
738
+ resolveReady: () => resolveReady(),
739
+ stopping: false,
740
+ awaited: true,
741
+ incoming: new IncomingRequests(),
742
+ slowStart: null
743
+ };
744
+ const context = {
745
+ id: this.id,
746
+ shards: this.shards,
747
+ shardCount: this.shardCount,
748
+ pingTimeout: this.ping.enabled ? this.ping.timeout + this.ping.interval : null,
749
+ requestTimeout: this.manager.requestTimeout,
750
+ messageHandler: this.manager.codec.handler.name,
751
+ transformers: this.manager.codec.transformers.map((transformer) => transformer.name)
752
+ };
753
+ instance.transport = this.manager.strategy.spawn(context, {
754
+ message: (data) => void this.#receive(instance, data),
755
+ exit: (code) => this.#exited(instance, code),
756
+ error: (error) => this.#failed(instance, error)
757
+ }, { env: this.manager.spawnEnv });
758
+ const estimate = this.manager.readyEstimate;
759
+ if (estimate !== null) {
760
+ instance.slowStart = setTimeout(() => {
761
+ instance.slowStart = null;
762
+ if (instance.everReady || instance.exited) return;
763
+ const elapsed = Date.now() - instance.startedAt;
764
+ this.emit("slowStart", elapsed, estimate);
765
+ this.manager.emit("shardSlowStart", this, elapsed, estimate);
766
+ }, estimate * (1 + this.manager.readyHintMargin));
767
+ instance.slowStart.unref();
768
+ }
769
+ this.emit("spawn");
770
+ this.manager.emit("shardCreate", this);
771
+ return instance;
772
+ }
773
+ async #awaitReady(instance, timeout) {
774
+ let timer;
775
+ try {
776
+ await Promise.race([
777
+ instance.ready,
778
+ instance.exit.then((code) => {
779
+ throw new ShardSpawnError(this.id, code);
780
+ }),
781
+ new Promise((_resolve, reject) => {
782
+ timer = setTimeout(() => reject(new ShardUnavailableError(this.id, `not ready within ${timeout}ms`)), timeout);
783
+ })
784
+ ]);
785
+ this.manager.recordReadyTime(Date.now() - instance.startedAt);
786
+ } catch (error) {
787
+ instance.stopping = true;
788
+ await this.#kill(instance);
789
+ if (this.#active === instance) this.#active = null;
790
+ throw error;
791
+ } finally {
792
+ clearTimeout(timer);
793
+ instance.awaited = false;
794
+ }
795
+ }
796
+ async #write(instance, packet) {
797
+ const transport = instance?.transport;
798
+ if (!transport || instance.exited) throw new ShardUnavailableError(this.id, "it is not running");
799
+ await transport.send(await this.manager.codec.encode(packet, { channelId: this.id }));
800
+ }
801
+ async #receive(instance, data) {
802
+ let packet;
803
+ try {
804
+ packet = await this.manager.codec.decode(data, { channelId: this.id });
805
+ } catch (error) {
806
+ this.manager.reportInvalidMessage(this, error);
807
+ return;
808
+ }
809
+ const write = (reply) => this.#write(instance, reply);
810
+ switch (packet.op) {
811
+ case Op.Signal:
812
+ instance.status = packet.status;
813
+ if (packet.status === ShardStatus.Ready && !instance.everReady) {
814
+ instance.everReady = true;
815
+ instance.awaited = false;
816
+ if (instance.slowStart) clearTimeout(instance.slowStart);
817
+ instance.resolveReady();
818
+ }
819
+ if (instance === this.#active) this.#setStatus(packet.status);
820
+ break;
821
+ case Op.Pong:
822
+ if (instance === this.#active) {
823
+ this.ping.receive(packet.sentAt);
824
+ this.emit("ping", this.ping.latency);
825
+ this.manager.emit("shardPing", this, this.ping.latency);
826
+ }
827
+ break;
828
+ case Op.Message:
829
+ if (packet.to === void 0) {
830
+ this.emit("message", packet.body);
831
+ this.manager.emit("message", packet.body, this);
832
+ } else await this.manager.route(packet.body, packet.to, this.id).catch((error) => this.manager.reportError(error));
833
+ break;
834
+ case Op.Request: {
835
+ const { to, timeout, partial, system } = packet;
836
+ const handler = system ? (body, { signal }) => this.manager.handleSystem(system, body, this, signal) : to === void 0 ? this.manager.requestHandler : (body, { signal }) => this.manager.forward(body, to, this.id, {
837
+ timeout,
838
+ signal,
839
+ partial
840
+ });
841
+ await instance.incoming.handle(write, packet, handler, { channel: this });
842
+ break;
843
+ }
844
+ case Op.Reply:
845
+ this.#outgoing.settle(packet);
846
+ break;
847
+ case Op.Abort:
848
+ instance.incoming.abort(packet.nonce);
849
+ break;
850
+ default: this.manager.reportInvalidMessage(this, /* @__PURE__ */ new TypeError(`Unexpected packet ${packet.op}`));
851
+ }
852
+ }
853
+ #setStatus(status) {
854
+ if (this.status === status) return;
855
+ this.status = status;
856
+ this.emit("status", status);
857
+ this.manager.emit("shardStatus", this, status);
858
+ switch (status) {
859
+ case ShardStatus.Ready:
860
+ this.manager.supervisor.ready(this.id);
861
+ this.ping.start();
862
+ for (const waiter of this.#waiters) waiter.resolve();
863
+ this.emit("ready");
864
+ this.manager.emit("shardReady", this);
865
+ break;
866
+ case ShardStatus.Disconnected:
867
+ this.emit("disconnect");
868
+ this.manager.emit("shardDisconnect", this);
869
+ break;
870
+ case ShardStatus.Reconnecting:
871
+ this.emit("reconnecting");
872
+ this.manager.emit("shardReconnecting", this);
873
+ break;
874
+ case ShardStatus.Idle: this.ping.stop();
875
+ }
876
+ }
877
+ #estimatedRemaining() {
878
+ const estimate = this.manager.readyEstimate;
879
+ const active = this.#active;
880
+ if (estimate === null || !active || this.status !== ShardStatus.Starting) return null;
881
+ const remaining = estimate - (Date.now() - active.startedAt) - estimate * this.manager.readyHintMargin;
882
+ return remaining > 0 ? Math.round(remaining) : null;
883
+ }
884
+ #unresponsive() {
885
+ if (this.listenerCount("unresponsive") > 0 || this.manager.listenerCount("shardUnresponsive") > 0) {
886
+ this.emit("unresponsive");
887
+ this.manager.emit("shardUnresponsive", this);
888
+ } else this.restart().catch((error) => this.manager.reportError(error));
889
+ }
890
+ async #stop(instance, timeout) {
891
+ instance.stopping = true;
892
+ if (instance.exited) return;
893
+ const asked = await this.#write(instance, { op: Op.Close }).then(() => true, () => false);
894
+ const timer = setTimeout(() => void this.#kill(instance), asked ? timeout : 0);
895
+ await instance.exit;
896
+ clearTimeout(timer);
897
+ }
898
+ async #kill(instance) {
899
+ instance.stopping = true;
900
+ if (!instance.exited) await instance.transport?.kill();
901
+ }
902
+ #failed(instance, error) {
903
+ const spawnError = new ShardSpawnError(this.id, null, error);
904
+ if (instance.everReady) this.manager.reportShardError(this, error);
905
+ else this.manager.reportShardError(this, spawnError);
906
+ }
907
+ #exited(instance, code) {
908
+ if (instance.exited) return;
909
+ instance.exited = true;
910
+ if (instance.slowStart) clearTimeout(instance.slowStart);
911
+ instance.incoming.abortAll();
912
+ this.#outgoing.rejectOwner(instance, new ShardUnavailableError(this.id, "it stopped"));
913
+ instance.resolveExit(code);
914
+ if (instance !== this.#active) return;
915
+ this.#active = null;
916
+ const previous = instance.status;
917
+ this.#setStatus(ShardStatus.Idle);
918
+ this.emit("exit", code);
919
+ this.manager.emit("shardExit", this, code);
920
+ if (!instance.everReady && !instance.stopping && code !== 0) this.manager.reportShardError(this, new ShardSpawnError(this.id, code));
921
+ if (instance.stopping || instance.awaited) return;
922
+ this.manager.supervise(this, previous);
923
+ }
924
+ #rejectWaiters(error) {
925
+ for (const waiter of this.#waiters) waiter.reject(error);
926
+ }
927
+ };
928
+
929
+ //#endregion
930
+ //#region src/messages/MessageHandler.ts
931
+ /**
932
+ * Serializes packets as JSON: every body must be JSON-serializable. The default.
933
+ */
934
+ var JsonMessageHandler = class {
935
+ name = "json";
936
+ serialize(packet) {
937
+ return JSON.stringify(packet);
938
+ }
939
+ deserialize(data) {
940
+ return JSON.parse(typeof data === "string" ? data : new TextDecoder().decode(data));
941
+ }
942
+ };
943
+ /**
944
+ * Serializes packets with `node:v8`, which keeps `Map`s, `Set`s, `Date`s, `BigInt`s, typed arrays, and circular
945
+ * references.
946
+ */
947
+ var V8MessageHandler = class {
948
+ name = "v8";
949
+ serialize(packet) {
950
+ return serialize(packet);
951
+ }
952
+ deserialize(data) {
953
+ if (!(data instanceof Uint8Array)) throw new TypeError("V8MessageHandler can only read binary data");
954
+ return deserialize(data);
955
+ }
956
+ };
957
+ /**
958
+ * Passes packets as they are, for channels that clone values themselves: worker threads, and processes with the
959
+ * `advanced` IPC serialization. It cannot be combined with transformers, nor cross the network.
960
+ */
961
+ var RawMessageHandler = class {
962
+ name = "raw";
963
+ serialize(packet) {
964
+ return packet;
965
+ }
966
+ deserialize(data) {
967
+ return data;
968
+ }
969
+ };
970
+ const handlers = /* @__PURE__ */ new Map([
971
+ ["json", () => new JsonMessageHandler()],
972
+ ["v8", () => new V8MessageHandler()],
973
+ ["raw", () => new RawMessageHandler()]
974
+ ]);
975
+ /**
976
+ * Registers a message handler under its name, so managers and shards can refer to it by name.
977
+ *
978
+ * @param name The name of the handler, the same as its `name`.
979
+ * @param factory Builds the handler.
980
+ */
981
+ function registerMessageHandler(name, factory) {
982
+ handlers.set(name, factory);
983
+ }
984
+ /**
985
+ * Resolves a message handler, or the name of a registered one.
986
+ *
987
+ * @param handler The handler, or its name.
988
+ */
989
+ function resolveMessageHandler(handler) {
990
+ if (typeof handler !== "string") return handler;
991
+ const factory = handlers.get(handler);
992
+ if (!factory) throw new RangeError(`There is no message handler named "${handler}"`);
993
+ return factory();
994
+ }
995
+
996
+ //#endregion
997
+ //#region src/messages/MessageTransformer.ts
998
+ const gzipAsync = promisify(gzip);
999
+ const gunzipAsync = promisify(gunzip);
1000
+ const brotliCompressAsync = promisify(brotliCompress);
1001
+ const brotliDecompressAsync = promisify(brotliDecompress);
1002
+ /**
1003
+ * Compresses messages with gzip.
1004
+ */
1005
+ var GzipTransformer = class {
1006
+ name = "gzip";
1007
+ options;
1008
+ /**
1009
+ * @param options The compression options, e.g. its `level`.
1010
+ */
1011
+ constructor(options = {}) {
1012
+ this.options = options;
1013
+ }
1014
+ write(data) {
1015
+ return gzipAsync(data, this.options);
1016
+ }
1017
+ read(data) {
1018
+ return gunzipAsync(data);
1019
+ }
1020
+ };
1021
+ /**
1022
+ * Compresses messages with Brotli, smaller than gzip at a higher CPU cost.
1023
+ */
1024
+ var BrotliTransformer = class {
1025
+ name = "brotli";
1026
+ write(data) {
1027
+ return brotliCompressAsync(data);
1028
+ }
1029
+ read(data) {
1030
+ return brotliDecompressAsync(data);
1031
+ }
1032
+ };
1033
+ const transformers = /* @__PURE__ */ new Map([["gzip", () => new GzipTransformer()], ["brotli", () => new BrotliTransformer()]]);
1034
+ /**
1035
+ * Registers a message transformer under its name, so managers and shards can refer to it by name.
1036
+ *
1037
+ * @param name The name of the transformer, the same as its `name`.
1038
+ * @param factory Builds the transformer, e.g. with a key read from the environment.
1039
+ */
1040
+ function registerMessageTransformer(name, factory) {
1041
+ transformers.set(name, factory);
1042
+ }
1043
+ /**
1044
+ * Resolves a message transformer, or the name of a registered one.
1045
+ *
1046
+ * @param transformer The transformer, or its name.
1047
+ */
1048
+ function resolveMessageTransformer(transformer) {
1049
+ if (typeof transformer !== "string") return transformer;
1050
+ const factory = transformers.get(transformer);
1051
+ if (!factory) throw new RangeError(`There is no message transformer named "${transformer}"`);
1052
+ return factory();
1053
+ }
1054
+
1055
+ //#endregion
1056
+ //#region src/strategies/ChannelStrategy.ts
1057
+ /**
1058
+ * The environment variable carrying the {@link ShardContext} of a shard.
1059
+ */
1060
+ const ShardContextVariable = "WOLFSTAR_SHARDER";
1061
+ /**
1062
+ * Serializes a shard's context for its environment or worker data.
1063
+ *
1064
+ * @internal
1065
+ */
1066
+ function encodeContext(context) {
1067
+ return JSON.stringify(context);
1068
+ }
1069
+
1070
+ //#endregion
1071
+ //#region src/ShardClient.ts
1072
+ const MaxTimeout = 2147483647;
1073
+ /**
1074
+ * The shard's side of the sharder: it tells the manager its status, and messages the manager and the other shards.
1075
+ *
1076
+ * @example
1077
+ * ```ts
1078
+ * const shard = new ShardClient();
1079
+ * const client = new GatewayClient({
1080
+ * ...options,
1081
+ * ...shard.gatewayOptions,
1082
+ * gateway: { buildIdentifyThrottler: () => shard.identifyThrottler },
1083
+ * });
1084
+ * // Reuse the manager's `GET /gateway/bot` rather than requesting it again.
1085
+ * client.gateway.fetchGatewayInformation = () => shard.fetchGatewayInformation();
1086
+ * shard.setRequestHandler((body) => ...);
1087
+ * ```
1088
+ */
1089
+ var ShardClient = class ShardClient extends EventEmitter {
1090
+ /**
1091
+ * The context of this process or thread, when a {@link ShardManager} spawned it; `null` otherwise.
1092
+ */
1093
+ static get context() {
1094
+ const raw = isMainThread ? process.env[ShardContextVariable] : workerData?.[ShardContextVariable];
1095
+ return typeof raw === "string" ? JSON.parse(raw) : null;
1096
+ }
1097
+ /**
1098
+ * Registers a message handler the manager can refer to by name. The sharder RFC's API.
1099
+ *
1100
+ * @param name The name of the handler.
1101
+ * @param factory Builds the handler.
1102
+ */
1103
+ static registerMessageHandler(name, factory) {
1104
+ registerMessageHandler(name, factory);
1105
+ return ShardClient;
1106
+ }
1107
+ /**
1108
+ * Registers a message transformer the manager can refer to by name. The sharder RFC's API.
1109
+ *
1110
+ * @param name The name of the transformer.
1111
+ * @param factory Builds the transformer.
1112
+ */
1113
+ static registerMessageTransformer(name, factory) {
1114
+ registerMessageTransformer(name, factory);
1115
+ return ShardClient;
1116
+ }
1117
+ /**
1118
+ * The ID of the shard, its channel's in the manager.
1119
+ */
1120
+ id;
1121
+ /**
1122
+ * The IDs of the gateway shards the shard connects.
1123
+ */
1124
+ shards;
1125
+ /**
1126
+ * The total number of gateway shards, across every shard.
1127
+ */
1128
+ shardCount;
1129
+ requestTimeout;
1130
+ /**
1131
+ * The last status signalled to the manager.
1132
+ */
1133
+ status = ShardStatus.Starting;
1134
+ /**
1135
+ * When the manager last pinged the shard, `null` before its first ping.
1136
+ */
1137
+ lastPingTimestamp = null;
1138
+ #transport;
1139
+ #codec;
1140
+ #outgoing = new OutgoingRequests();
1141
+ #incoming = new IncomingRequests();
1142
+ #pingTimeout;
1143
+ #watchdog = null;
1144
+ #requestHandler = null;
1145
+ #closeHandler = null;
1146
+ #shardHandler = null;
1147
+ constructor(options = {}) {
1148
+ super();
1149
+ const context = options.context ?? ShardClient.context;
1150
+ if (!context) throw new Error("This process or thread was not spawned by a ShardManager");
1151
+ this.id = context.id;
1152
+ this.shards = context.shards;
1153
+ this.shardCount = context.shardCount;
1154
+ this.requestTimeout = context.requestTimeout;
1155
+ this.#codec = new PacketCodec(resolveMessageHandler(options.messageHandler ?? context.messageHandler), (options.transformers ?? context.transformers).map(resolveMessageTransformer));
1156
+ this.#transport = options.transport ?? defaultTransport(context);
1157
+ this.#transport.onMessage((data) => void this.#receive(data));
1158
+ this.#transport.onDisconnect(() => {
1159
+ this.#stopWatchdog();
1160
+ if (this.listenerCount("disconnect") > 0) this.emit("disconnect");
1161
+ else this.#transport.exit(0);
1162
+ });
1163
+ this.#pingTimeout = context.pingTimeout;
1164
+ this.#signal(ShardStatus.Starting).catch((error) => this.#report(error));
1165
+ }
1166
+ /**
1167
+ * The ID of the shard's process.
1168
+ */
1169
+ get pid() {
1170
+ return process.pid;
1171
+ }
1172
+ /**
1173
+ * The ID of the shard's thread, `0` for a process's main thread.
1174
+ */
1175
+ get threadId() {
1176
+ return threadId;
1177
+ }
1178
+ /**
1179
+ * The gateway shards to connect, to spread into `GatewayClient`'s (or `@discordjs/ws`'s) options.
1180
+ */
1181
+ get gatewayOptions() {
1182
+ return {
1183
+ shardIds: [...this.shards],
1184
+ shardCount: this.shardCount
1185
+ };
1186
+ }
1187
+ /**
1188
+ * Paces the identifies of the gateway shards across every shard, through the manager: pass it as `@discordjs/ws`'s
1189
+ * `buildIdentifyThrottler`. The manager then needs no spawn delay.
1190
+ */
1191
+ get identifyThrottler() {
1192
+ return { waitForIdentify: async (shardId, signal) => {
1193
+ await this.#system("identify", shardId, {
1194
+ timeout: MaxTimeout,
1195
+ signal
1196
+ });
1197
+ } };
1198
+ }
1199
+ /**
1200
+ * Gets `GET /gateway/bot` from the manager, fetched once for every shard, e.g. to replace `@discordjs/ws`'s
1201
+ * `WebSocketManager#fetchGatewayInformation`.
1202
+ */
1203
+ fetchGatewayInformation() {
1204
+ return this.#system("gatewayInformation", null);
1205
+ }
1206
+ /**
1207
+ * Tells the manager that the shard is ready, e.g. once its gateway shards are. The manager spawns the next shard
1208
+ * only then.
1209
+ */
1210
+ ready() {
1211
+ if (!this.#watchdog && this.#pingTimeout !== null) {
1212
+ this.#watchdog = setTimeout(() => {
1213
+ this.#watchdog = null;
1214
+ this.emit("managerUnresponsive");
1215
+ }, this.#pingTimeout);
1216
+ this.#watchdog.unref();
1217
+ }
1218
+ return this.#signal(ShardStatus.Ready);
1219
+ }
1220
+ /**
1221
+ * Tells the manager that the shard lost what it serves, e.g. the gateway. Messages for it wait until it is ready
1222
+ * again.
1223
+ */
1224
+ disconnected() {
1225
+ return this.#signal(ShardStatus.Disconnected);
1226
+ }
1227
+ /**
1228
+ * Tells the manager that the shard is reconnecting to what it serves.
1229
+ */
1230
+ reconnecting() {
1231
+ return this.#signal(ShardStatus.Reconnecting);
1232
+ }
1233
+ /**
1234
+ * Stops the shard for good: the manager does not respawn it.
1235
+ *
1236
+ * @param code The exit code.
1237
+ */
1238
+ async exit(code = 0) {
1239
+ await this.#stop(ShardStatus.Exiting, code);
1240
+ }
1241
+ /**
1242
+ * Stops the shard, for the manager to spawn it again.
1243
+ */
1244
+ async restart() {
1245
+ await this.#stop(ShardStatus.Restarting, 0);
1246
+ }
1247
+ /**
1248
+ * Sends a message to the manager, emitted as its `message` event, or to other shards.
1249
+ *
1250
+ * @param body The message.
1251
+ * @param to The ID of the shard to send it to, or `"all"`. Left out, it goes to the manager.
1252
+ */
1253
+ send(body, to) {
1254
+ return this.#write({
1255
+ op: Op.Message,
1256
+ body,
1257
+ to
1258
+ });
1259
+ }
1260
+ /**
1261
+ * Sends a request to the manager or to another shard.
1262
+ *
1263
+ * @param body The request.
1264
+ * @param options Where it goes (the manager by default), its timeout, and its abort signal.
1265
+ */
1266
+ request(body, options = {}) {
1267
+ return this.#outgoing.request((packet) => this.#write(packet), {
1268
+ body,
1269
+ to: options.to
1270
+ }, options.timeout ?? this.requestTimeout, options.signal);
1271
+ }
1272
+ async broadcastRequest(body, options = {}) {
1273
+ const reply = await this.#outgoing.request((packet) => this.#write(packet), {
1274
+ body,
1275
+ to: "all",
1276
+ partial: options.partial
1277
+ }, options.timeout ?? this.requestTimeout, options.signal);
1278
+ return options.partial ? deserializeSettled(reply) : reply;
1279
+ }
1280
+ /**
1281
+ * {@link ShardClient.send}, resolving with a `Result` rather than rejecting.
1282
+ */
1283
+ trySend(body, to) {
1284
+ return toResult(() => this.send(body, to));
1285
+ }
1286
+ /**
1287
+ * {@link ShardClient.request}, resolving with a `Result` rather than rejecting.
1288
+ */
1289
+ tryRequest(body, options) {
1290
+ return toResult(() => this.request(body, options));
1291
+ }
1292
+ /**
1293
+ * {@link ShardClient.broadcastRequest}, with one `Result` per shard, by shard ID. The broadcast itself failing
1294
+ * (e.g. the manager not answering) is the outer `Err`.
1295
+ */
1296
+ tryBroadcastRequest(body, options) {
1297
+ return toResult(async () => settledToResults(await this.broadcastRequest(body, {
1298
+ ...options,
1299
+ partial: true
1300
+ })));
1301
+ }
1302
+ /**
1303
+ * {@link ShardClient.control}, resolving with a `Result` rather than rejecting.
1304
+ */
1305
+ tryControl(request, options) {
1306
+ return toResult(() => this.control(request, options));
1307
+ }
1308
+ /**
1309
+ * Asks the manager to start, close, or restart shards or gateway shards.
1310
+ *
1311
+ * @param request What to do, and to what.
1312
+ * @param options The timeout and abort signal of the request.
1313
+ * @example
1314
+ * ```ts
1315
+ * await shard.control({ action: "restart", target: { channel: "all" } });
1316
+ * await shard.control({ action: "restart", target: { shard: 12 } });
1317
+ * ```
1318
+ */
1319
+ async control(request, options) {
1320
+ await this.#system("control", request, options);
1321
+ }
1322
+ /**
1323
+ * Sets the handler answering the requests of the manager and of the other shards.
1324
+ *
1325
+ * @param handler The handler; its return value is the reply. `from` is the shard asking, `null` for the manager.
1326
+ */
1327
+ setRequestHandler(handler) {
1328
+ this.#requestHandler = handler;
1329
+ return this;
1330
+ }
1331
+ /**
1332
+ * Sets what to do when the manager closes the shard, e.g. disconnect from the gateway. The shard exits once it
1333
+ * resolves; without a handler, it exits right away.
1334
+ *
1335
+ * @param handler The handler.
1336
+ */
1337
+ setCloseHandler(handler) {
1338
+ this.#closeHandler = handler;
1339
+ return this;
1340
+ }
1341
+ /**
1342
+ * Sets how the manager starts and closes the gateway shards of this shard, for `manager.startShard` & co.
1343
+ *
1344
+ * @param handler Starts or closes a gateway shard, resolving once done.
1345
+ */
1346
+ setShardHandler(handler) {
1347
+ this.#shardHandler = handler;
1348
+ return this;
1349
+ }
1350
+ #system(call, body, options = {}) {
1351
+ return this.#outgoing.request((packet) => this.#write(packet), {
1352
+ body,
1353
+ system: call
1354
+ }, options.timeout ?? this.requestTimeout, options.signal);
1355
+ }
1356
+ async #signal(status) {
1357
+ this.status = status;
1358
+ await this.#write({
1359
+ op: Op.Signal,
1360
+ status
1361
+ });
1362
+ }
1363
+ #stopWatchdog() {
1364
+ if (this.#watchdog) clearTimeout(this.#watchdog);
1365
+ this.#watchdog = null;
1366
+ }
1367
+ async #stop(status, code) {
1368
+ this.#stopWatchdog();
1369
+ await this.#signal(status).catch(() => void 0);
1370
+ this.#transport.exit(code);
1371
+ }
1372
+ async #write(packet) {
1373
+ await this.#transport.send(await this.#codec.encode(packet, { channelId: this.id }));
1374
+ }
1375
+ async #receive(data) {
1376
+ let packet;
1377
+ try {
1378
+ packet = await this.#codec.decode(data, { channelId: this.id });
1379
+ } catch (error) {
1380
+ if (this.listenerCount("invalidMessage") > 0) this.emit("invalidMessage", error);
1381
+ else console.error("The shard manager sent an invalid message:", error);
1382
+ return;
1383
+ }
1384
+ switch (packet.op) {
1385
+ case Op.Ping:
1386
+ this.lastPingTimestamp = Date.now();
1387
+ this.#watchdog?.refresh();
1388
+ await this.#write({
1389
+ op: Op.Pong,
1390
+ sentAt: packet.sentAt
1391
+ }).catch(() => void 0);
1392
+ break;
1393
+ case Op.Message:
1394
+ this.emit("message", packet.body, packet.from ?? null);
1395
+ break;
1396
+ case Op.Request: {
1397
+ const { system } = packet;
1398
+ const handler = system ? (body, { signal }) => this.#handleSystem(system, body, signal) : this.#requestHandler;
1399
+ await this.#incoming.handle((reply) => this.#write(reply), packet, handler, { from: packet.from ?? null });
1400
+ break;
1401
+ }
1402
+ case Op.Reply:
1403
+ this.#outgoing.settle(packet);
1404
+ break;
1405
+ case Op.Abort:
1406
+ this.#incoming.abort(packet.nonce);
1407
+ break;
1408
+ case Op.Close:
1409
+ try {
1410
+ await this.#closeHandler?.();
1411
+ } catch (error) {
1412
+ this.#report(error);
1413
+ }
1414
+ await this.exit(0);
1415
+ }
1416
+ }
1417
+ async #handleSystem(call, shardId, signal) {
1418
+ const action = call === "startShard" ? this.#shardHandler?.start : call === "closeShard" ? this.#shardHandler?.close : null;
1419
+ if (call !== "startShard" && call !== "closeShard") throw new Error(`The manager cannot send the ${call} system request`);
1420
+ if (!this.shards.includes(shardId)) throw new RangeError(`This shard does not connect the gateway shard ${shardId}`);
1421
+ if (!action) throw new Error(`There is no shard handler to ${call === "startShard" ? "start" : "close"} gateway shards`);
1422
+ await action.call(this.#shardHandler, shardId, { signal });
1423
+ return null;
1424
+ }
1425
+ #report(error) {
1426
+ if (this.listenerCount("error") > 0) this.emit("error", error);
1427
+ else console.error(error);
1428
+ }
1429
+ };
1430
+ function defaultTransport(context) {
1431
+ if (context.transport === "worker") {
1432
+ if (!parentPort) throw new Error("The shard is not running in a worker thread");
1433
+ const port = parentPort;
1434
+ return {
1435
+ send: async (data) => port.postMessage(data),
1436
+ onMessage: (listener) => port.on("message", listener),
1437
+ onDisconnect: () => void 0,
1438
+ exit: (code) => process.exit(code)
1439
+ };
1440
+ }
1441
+ if (!process.send) throw new Error("The shard has no IPC channel to its manager");
1442
+ return {
1443
+ send: (data) => new Promise((resolve, reject) => {
1444
+ if (!process.connected) {
1445
+ reject(/* @__PURE__ */ new Error("The IPC channel to the manager is closed"));
1446
+ return;
1447
+ }
1448
+ process.send(data, void 0, void 0, (error) => error ? reject(error) : resolve());
1449
+ }),
1450
+ onMessage: (listener) => process.on("message", listener),
1451
+ onDisconnect: (listener) => process.once("disconnect", listener),
1452
+ exit: (code) => process.exit(code)
1453
+ };
1454
+ }
1455
+
1456
+ //#endregion
1457
+ //#region src/strategies/ProcessStrategy.ts
1458
+ /**
1459
+ * The base of {@link ForkStrategy} and {@link ClusterStrategy}: a shard is a process with an IPC channel, using the
1460
+ * `advanced` serialization so binary data (and {@link RawMessageHandler}'s values) survive it.
1461
+ */
1462
+ var ProcessStrategy = class {
1463
+ spawn(context, events, options) {
1464
+ const child = this.createProcess({
1465
+ ...context,
1466
+ transport: "process"
1467
+ }, options);
1468
+ const process = "process" in child ? child.process : child;
1469
+ process.on("message", (data) => events.message(data));
1470
+ process.on("error", (error) => events.error(error));
1471
+ let exited = false;
1472
+ const exit = new Promise((resolve) => {
1473
+ process.once("exit", (code) => {
1474
+ exited = true;
1475
+ events.exit(code);
1476
+ resolve();
1477
+ });
1478
+ });
1479
+ return {
1480
+ pid: process.pid ?? null,
1481
+ send: (data) => new Promise((resolve, reject) => {
1482
+ if (!process.connected) {
1483
+ reject(/* @__PURE__ */ new Error("The shard's IPC channel is closed"));
1484
+ return;
1485
+ }
1486
+ process.send(data, void 0, void 0, (error) => error ? reject(error) : resolve());
1487
+ }),
1488
+ kill: async () => {
1489
+ if (!exited) process.kill();
1490
+ await exit;
1491
+ }
1492
+ };
1493
+ }
1494
+ };
1495
+
1496
+ //#endregion
1497
+ //#region src/strategies/ClusterStrategy.ts
1498
+ /**
1499
+ * Spawns every shard as a `node:cluster` worker. Unlike {@link ForkStrategy}, the shards share the ports they listen
1500
+ * on, and the primary balances the connections between them.
1501
+ */
1502
+ var ClusterStrategy = class extends ProcessStrategy {
1503
+ name = "cluster";
1504
+ options;
1505
+ constructor(options = {}) {
1506
+ super();
1507
+ this.options = options;
1508
+ }
1509
+ createProcess(context, options) {
1510
+ const { path, args, execArgv, env } = this.options;
1511
+ cluster.setupPrimary({
1512
+ exec: path instanceof URL ? fileURLToPath(path) : path,
1513
+ args: args ? [...args] : void 0,
1514
+ execArgv: execArgv ? [...execArgv] : void 0,
1515
+ serialization: "advanced"
1516
+ });
1517
+ return cluster.fork({
1518
+ ...env,
1519
+ ...options.env,
1520
+ [ShardContextVariable]: encodeContext(context)
1521
+ });
1522
+ }
1523
+ };
1524
+
1525
+ //#endregion
1526
+ //#region src/strategies/ForkStrategy.ts
1527
+ /**
1528
+ * Spawns every shard as a child process with `child_process.fork`. The default strategy.
1529
+ *
1530
+ * @remarks
1531
+ * Every shard is its own process: ports opened by the shards (e.g. an HTTP interactions server) must differ.
1532
+ */
1533
+ var ForkStrategy = class extends ProcessStrategy {
1534
+ name = "fork";
1535
+ options;
1536
+ constructor(options = {}) {
1537
+ super();
1538
+ this.options = options;
1539
+ }
1540
+ createProcess(context, options) {
1541
+ const { path = process.argv[1], args = [], execArgv, env } = this.options;
1542
+ return fork(path instanceof URL ? fileURLToPath(path) : path, args, {
1543
+ env: {
1544
+ ...process.env,
1545
+ ...env,
1546
+ ...options.env,
1547
+ [ShardContextVariable]: encodeContext(context)
1548
+ },
1549
+ execArgv: execArgv ? [...execArgv] : void 0,
1550
+ serialization: "advanced"
1551
+ });
1552
+ }
1553
+ };
1554
+
1555
+ //#endregion
1556
+ //#region src/network/Connection.ts
1557
+ /**
1558
+ * The frames exchanged between a manager and its proxies, and between proxies.
1559
+ *
1560
+ * @internal
1561
+ */
1562
+ const FrameType = {
1563
+ /**
1564
+ * Proxy → manager, JSON {@link HelloFrame}: authenticates, and lists the shards still running.
1565
+ */
1566
+ Hello: 0,
1567
+ /**
1568
+ * Manager → proxy, JSON {@link WelcomeFrame}: accepts the proxy, and lists the running shards to kill.
1569
+ */
1570
+ Welcome: 1,
1571
+ /**
1572
+ * Manager → proxy, JSON `{ reason }`: rejects the proxy.
1573
+ */
1574
+ Reject: 2,
1575
+ /**
1576
+ * Manager → proxy, JSON {@link SpawnFrame}.
1577
+ */
1578
+ Spawn: 3,
1579
+ /**
1580
+ * Proxy → manager, JSON `{ spawnId, pid, threadId }`: the shard was spawned.
1581
+ */
1582
+ Spawned: 4,
1583
+ /**
1584
+ * Manager → proxy, JSON `{ spawnId }`.
1585
+ */
1586
+ Kill: 5,
1587
+ /**
1588
+ * Proxy → manager, JSON `{ spawnId, code }`.
1589
+ */
1590
+ Exit: 6,
1591
+ /**
1592
+ * Proxy → manager, JSON `{ spawnId, message }`.
1593
+ */
1594
+ Error: 7,
1595
+ /**
1596
+ * Both ways: a spawn ID (u32), then the string data.
1597
+ */
1598
+ StringData: 8,
1599
+ /**
1600
+ * Both ways: a spawn ID (u32), then the binary data.
1601
+ */
1602
+ BinaryData: 9,
1603
+ Heartbeat: 10,
1604
+ /**
1605
+ * Proxy → manager, JSON `{ available }`: how many more shards the proxy takes, across all its managers.
1606
+ */
1607
+ Load: 11,
1608
+ /**
1609
+ * Proxy → manager, JSON `{ spawnId, ready }`: whether a shard is ready, for the peer directory.
1610
+ */
1611
+ Status: 12,
1612
+ /**
1613
+ * Manager → proxy, JSON {@link DirectoryFrame}: which proxy runs which ready shard, for peer routing.
1614
+ */
1615
+ Directory: 13,
1616
+ /**
1617
+ * Proxy → proxy, JSON `{ token, name }`: opens a peer connection.
1618
+ */
1619
+ PeerHello: 20,
1620
+ /**
1621
+ * Proxy → proxy, empty: accepts a peer connection.
1622
+ */
1623
+ PeerWelcome: 21,
1624
+ /**
1625
+ * Proxy → proxy: a {@link PeerHeader} (length-prefixed JSON), then the packet as a shard encoded it.
1626
+ */
1627
+ PeerPacket: 22
1628
+ };
1629
+ /**
1630
+ * Carries length-prefixed frames over a socket, with heartbeats.
1631
+ *
1632
+ * @internal
1633
+ */
1634
+ var Connection = class extends EventEmitter {
1635
+ socket;
1636
+ #buffer = Buffer.alloc(0);
1637
+ #closed = false;
1638
+ #heartbeat;
1639
+ #watchdog;
1640
+ constructor(socket, heartbeat) {
1641
+ super();
1642
+ this.socket = socket;
1643
+ socket.setNoDelay(true);
1644
+ socket.on("data", (chunk) => this.#read(chunk));
1645
+ socket.on("error", (error) => this.close(error));
1646
+ socket.on("close", () => this.close());
1647
+ this.#heartbeat = setInterval(() => void this.send(FrameType.Heartbeat).catch(() => void 0), heartbeat.interval);
1648
+ this.#heartbeat.unref();
1649
+ this.#watchdog = setTimeout(() => this.close(/* @__PURE__ */ new Error(`No heartbeat for ${heartbeat.timeout}ms`)), heartbeat.timeout);
1650
+ this.#watchdog.unref();
1651
+ }
1652
+ get closed() {
1653
+ return this.#closed;
1654
+ }
1655
+ send(type, payload = Buffer.alloc(0)) {
1656
+ if (this.#closed) return Promise.reject(/* @__PURE__ */ new Error("The connection is closed"));
1657
+ const header = Buffer.allocUnsafe(5);
1658
+ header.writeUInt32BE(payload.length + 1, 0);
1659
+ header.writeUInt8(type, 4);
1660
+ return new Promise((resolve, reject) => {
1661
+ this.socket.write(Buffer.concat([header, payload]), (error) => error ? reject(error) : resolve());
1662
+ });
1663
+ }
1664
+ sendJson(type, value) {
1665
+ return this.send(type, Buffer.from(JSON.stringify(value)));
1666
+ }
1667
+ sendPeer(header, data) {
1668
+ const json = Buffer.from(JSON.stringify(header));
1669
+ const length = Buffer.allocUnsafe(4);
1670
+ length.writeUInt32BE(json.length, 0);
1671
+ const body = typeof data === "string" ? Buffer.from(data) : data;
1672
+ return this.send(FrameType.PeerPacket, Buffer.concat([
1673
+ length,
1674
+ json,
1675
+ body
1676
+ ]));
1677
+ }
1678
+ sendData(spawnId, data) {
1679
+ const prefix = Buffer.allocUnsafe(4);
1680
+ prefix.writeUInt32BE(spawnId, 0);
1681
+ if (typeof data === "string") return this.send(FrameType.StringData, Buffer.concat([prefix, Buffer.from(data)]));
1682
+ if (data instanceof Uint8Array) return this.send(FrameType.BinaryData, Buffer.concat([prefix, data]));
1683
+ return Promise.reject(/* @__PURE__ */ new TypeError("Only string or binary data crosses the network: the raw message handler cannot"));
1684
+ }
1685
+ close(error) {
1686
+ if (this.#closed) return;
1687
+ this.#closed = true;
1688
+ clearInterval(this.#heartbeat);
1689
+ clearTimeout(this.#watchdog);
1690
+ this.socket.destroy();
1691
+ this.emit("close", error);
1692
+ }
1693
+ #read(chunk) {
1694
+ this.#watchdog.refresh();
1695
+ this.#buffer = this.#buffer.length === 0 ? chunk : Buffer.concat([this.#buffer, chunk]);
1696
+ while (this.#buffer.length >= 4) {
1697
+ const length = this.#buffer.readUInt32BE(0);
1698
+ if (this.#buffer.length < 4 + length) return;
1699
+ const type = this.#buffer.readUInt8(4);
1700
+ const payload = this.#buffer.subarray(5, 4 + length);
1701
+ this.#buffer = this.#buffer.subarray(4 + length);
1702
+ if (type !== FrameType.Heartbeat) this.emit("frame", type, payload);
1703
+ }
1704
+ }
1705
+ };
1706
+ /**
1707
+ * Reads the spawn ID and the data of a data frame.
1708
+ *
1709
+ * @internal
1710
+ */
1711
+ function readData(type, payload) {
1712
+ const spawnId = payload.readUInt32BE(0);
1713
+ const body = payload.subarray(4);
1714
+ return {
1715
+ spawnId,
1716
+ data: type === FrameType.StringData ? body.toString() : new Uint8Array(body)
1717
+ };
1718
+ }
1719
+ /**
1720
+ * Reads the header and the data of a peer packet.
1721
+ *
1722
+ * @internal
1723
+ */
1724
+ function readPeer(payload) {
1725
+ const length = payload.readUInt32BE(0);
1726
+ const header = JSON.parse(payload.subarray(4, 4 + length).toString());
1727
+ const body = payload.subarray(4 + length);
1728
+ return {
1729
+ header,
1730
+ data: header.binary ? new Uint8Array(body) : body.toString()
1731
+ };
1732
+ }
1733
+ /**
1734
+ * Compares two tokens in constant time.
1735
+ *
1736
+ * @internal
1737
+ */
1738
+ function tokensMatch(expected, actual) {
1739
+ if (typeof actual !== "string") return false;
1740
+ return timingSafeEqual(sha256(expected), sha256(actual));
1741
+ }
1742
+ function sha256(value) {
1743
+ return createHash("sha256").update(value).digest();
1744
+ }
1745
+
1746
+ //#endregion
1747
+ //#region src/strategies/NetworkStrategy.ts
1748
+ /**
1749
+ * Spawns shards on other machines, through {@link ShardManagerProxy}s connecting to the manager: the sharder RFC's
1750
+ * network strategy.
1751
+ *
1752
+ * @remarks
1753
+ * Each spawn goes to the connected proxy with the most room, as the proxies report it across all the managers they
1754
+ * serve; without room, it waits for a proxy, or for the spawn timeout. A proxy losing its connection has
1755
+ * `reconnectGrace` to come back with its shards, after which they are spawned elsewhere; its stale shards are killed
1756
+ * when it comes back later. Proxies coming back are not rebalanced: they take the next spawns.
1757
+ *
1758
+ * The strategy keeps the proxies accepting peers informed of which proxy runs which ready shard, so they carry the
1759
+ * messages between their shards directly.
1760
+ */
1761
+ var NetworkStrategy = class {
1762
+ name = "network";
1763
+ options;
1764
+ /**
1765
+ * The ID of the manager, see {@link NetworkStrategyOptions.id}.
1766
+ */
1767
+ id;
1768
+ #proxies = /* @__PURE__ */ new Map();
1769
+ #spawns = /* @__PURE__ */ new Map();
1770
+ #pending = [];
1771
+ #server = null;
1772
+ #nextSpawnId = 1;
1773
+ #directoryQueued = false;
1774
+ constructor(options) {
1775
+ this.options = options;
1776
+ this.id = options.id ?? randomUUID();
1777
+ }
1778
+ /**
1779
+ * The proxies, connected or within their reconnect grace.
1780
+ */
1781
+ get proxies() {
1782
+ return [...this.#proxies.values()].map((proxy) => ({
1783
+ name: proxy.name,
1784
+ host: proxy.host,
1785
+ capacity: proxy.capacity,
1786
+ available: proxy.available,
1787
+ load: proxy.spawns.size,
1788
+ peer: proxy.peer
1789
+ }));
1790
+ }
1791
+ /**
1792
+ * The port the strategy listens on, once initialized.
1793
+ */
1794
+ get port() {
1795
+ const address = this.#server?.address();
1796
+ return typeof address === "object" && address ? address.port : null;
1797
+ }
1798
+ /**
1799
+ * Drops the connection of a proxy, which reconnects: its shards are kept if it comes back within the grace.
1800
+ *
1801
+ * @param name The name of the proxy.
1802
+ * @returns Whether a proxy of that name was connected.
1803
+ */
1804
+ disconnectProxy(name) {
1805
+ const connection = this.#proxies.get(name)?.connection;
1806
+ connection?.close();
1807
+ return connection !== void 0 && connection !== null;
1808
+ }
1809
+ async init() {
1810
+ if (this.#server) return;
1811
+ const onSocket = (socket) => this.#accept(socket);
1812
+ const server = this.options.tls ? createServer$1(this.options.tls, onSocket) : createServer(onSocket);
1813
+ await new Promise((resolve, reject) => {
1814
+ server.once("error", reject);
1815
+ server.listen(this.options.port, this.options.host, () => {
1816
+ server.off("error", reject);
1817
+ resolve();
1818
+ });
1819
+ });
1820
+ this.#server = server;
1821
+ }
1822
+ async destroy() {
1823
+ for (const proxy of this.#proxies.values()) proxy.connection?.close();
1824
+ this.#proxies.clear();
1825
+ const server = this.#server;
1826
+ this.#server = null;
1827
+ if (server) await new Promise((resolve) => server.close(() => resolve()));
1828
+ }
1829
+ spawn(context, events, options) {
1830
+ if (context.messageHandler === "raw") throw new TypeError("The raw message handler cannot cross the network");
1831
+ let resolveExit;
1832
+ const spawn = {
1833
+ id: this.#nextSpawnId++,
1834
+ context,
1835
+ env: options.env,
1836
+ events,
1837
+ proxy: null,
1838
+ pid: null,
1839
+ threadId: null,
1840
+ ready: false,
1841
+ exited: false,
1842
+ exit: new Promise((resolve) => {
1843
+ resolveExit = resolve;
1844
+ }),
1845
+ resolveExit: () => resolveExit(),
1846
+ grace: null
1847
+ };
1848
+ this.#spawns.set(spawn.id, spawn);
1849
+ this.#place(spawn);
1850
+ return {
1851
+ get pid() {
1852
+ return spawn.pid;
1853
+ },
1854
+ get threadId() {
1855
+ return spawn.threadId;
1856
+ },
1857
+ get host() {
1858
+ return spawn.proxy?.name ?? null;
1859
+ },
1860
+ send: async (data) => {
1861
+ const connection = spawn.proxy?.connection;
1862
+ if (!connection || connection.closed) throw new Error("The shard's proxy is not connected");
1863
+ await connection.sendData(spawn.id, data);
1864
+ },
1865
+ kill: async () => {
1866
+ if (spawn.exited) return;
1867
+ const connection = spawn.proxy?.connection;
1868
+ if (connection && !connection.closed) {
1869
+ await connection.sendJson(FrameType.Kill, { spawnId: spawn.id }).catch(() => void 0);
1870
+ await Promise.race([spawn.exit, new Promise((resolve) => setTimeout(resolve, 1e4).unref())]);
1871
+ }
1872
+ this.#exit(spawn, null);
1873
+ }
1874
+ };
1875
+ }
1876
+ #place(spawn) {
1877
+ let best = null;
1878
+ for (const proxy of this.#proxies.values()) {
1879
+ if (!proxy.connection || proxy.available <= 0) continue;
1880
+ if (!best || proxy.available / proxy.capacity > best.available / best.capacity) best = proxy;
1881
+ }
1882
+ if (!best) {
1883
+ this.#pending.push(spawn);
1884
+ return;
1885
+ }
1886
+ spawn.proxy = best;
1887
+ best.spawns.add(spawn);
1888
+ best.available--;
1889
+ const frame = {
1890
+ spawnId: spawn.id,
1891
+ context: spawn.context,
1892
+ env: spawn.env
1893
+ };
1894
+ best.connection.sendJson(FrameType.Spawn, frame).catch(() => void 0);
1895
+ }
1896
+ #drain() {
1897
+ const pending = this.#pending.splice(0);
1898
+ for (const spawn of pending) if (!spawn.exited) this.#place(spawn);
1899
+ }
1900
+ #exit(spawn, code) {
1901
+ if (spawn.exited) return;
1902
+ spawn.exited = true;
1903
+ if (spawn.grace) clearTimeout(spawn.grace);
1904
+ const { proxy } = spawn;
1905
+ proxy?.spawns.delete(spawn);
1906
+ if (proxy && !proxy.connection && proxy.spawns.size === 0) this.#proxies.delete(proxy.name);
1907
+ this.#spawns.delete(spawn.id);
1908
+ const index = this.#pending.indexOf(spawn);
1909
+ if (index !== -1) this.#pending.splice(index, 1);
1910
+ spawn.resolveExit();
1911
+ spawn.events.exit(code);
1912
+ if (spawn.ready) this.#queueDirectory();
1913
+ this.#drain();
1914
+ }
1915
+ #accept(socket) {
1916
+ const heartbeat = {
1917
+ interval: this.options.heartbeat?.interval ?? 15e3,
1918
+ timeout: this.options.heartbeat?.timeout ?? 45e3
1919
+ };
1920
+ const connection = new Connection(socket, heartbeat);
1921
+ let proxy = null;
1922
+ connection.on("frame", (type, payload) => {
1923
+ if (!proxy) {
1924
+ proxy = this.#hello(connection, type, payload);
1925
+ return;
1926
+ }
1927
+ this.#frame(proxy, type, payload);
1928
+ });
1929
+ connection.on("close", () => {
1930
+ if (proxy?.connection === connection) this.#lost(proxy);
1931
+ });
1932
+ }
1933
+ #hello(connection, type, payload) {
1934
+ let hello;
1935
+ try {
1936
+ hello = JSON.parse(payload.toString());
1937
+ } catch {
1938
+ connection.close();
1939
+ return null;
1940
+ }
1941
+ if (type !== FrameType.Hello || !tokensMatch(this.options.token, hello.token)) {
1942
+ connection.sendJson(FrameType.Reject, { reason: "Unauthorized" }).finally(() => connection.close());
1943
+ return null;
1944
+ }
1945
+ const known = this.#proxies.get(hello.name);
1946
+ if (known?.connection && !known.connection.closed) {
1947
+ connection.sendJson(FrameType.Reject, { reason: `A proxy named ${hello.name} is connected` }).finally(() => connection.close());
1948
+ return null;
1949
+ }
1950
+ const proxy = known ?? {
1951
+ name: hello.name,
1952
+ host: null,
1953
+ capacity: hello.capacity,
1954
+ available: hello.available,
1955
+ peer: null,
1956
+ connection: null,
1957
+ spawns: /* @__PURE__ */ new Set()
1958
+ };
1959
+ proxy.connection = connection;
1960
+ proxy.capacity = hello.capacity;
1961
+ proxy.available = hello.available;
1962
+ proxy.peer = hello.peer;
1963
+ proxy.host = connection.socket.remoteAddress ?? null;
1964
+ this.#proxies.set(proxy.name, proxy);
1965
+ const running = new Set(hello.running);
1966
+ for (const spawn of proxy.spawns) if (running.has(spawn.id)) {
1967
+ if (spawn.grace) clearTimeout(spawn.grace);
1968
+ spawn.grace = null;
1969
+ } else this.#exit(spawn, null);
1970
+ const welcome = {
1971
+ kill: hello.running.filter((spawnId) => this.#spawns.get(spawnId)?.proxy !== proxy),
1972
+ managerId: this.id
1973
+ };
1974
+ connection.sendJson(FrameType.Welcome, welcome).catch(() => void 0);
1975
+ this.#queueDirectory();
1976
+ this.#drain();
1977
+ return proxy;
1978
+ }
1979
+ #frame(proxy, type, payload) {
1980
+ if (type === FrameType.StringData || type === FrameType.BinaryData) {
1981
+ const { spawnId, data } = readData(type, payload);
1982
+ const spawn = this.#spawns.get(spawnId);
1983
+ if (spawn?.proxy === proxy) spawn.events.message(data);
1984
+ return;
1985
+ }
1986
+ const body = JSON.parse(payload.toString());
1987
+ if (type === FrameType.Load) {
1988
+ proxy.available = body.available ?? proxy.available;
1989
+ this.#drain();
1990
+ return;
1991
+ }
1992
+ const spawn = this.#spawns.get(body.spawnId);
1993
+ if (spawn?.proxy !== proxy) return;
1994
+ switch (type) {
1995
+ case FrameType.Spawned:
1996
+ spawn.pid = body.pid ?? null;
1997
+ spawn.threadId = body.threadId ?? null;
1998
+ break;
1999
+ case FrameType.Status:
2000
+ spawn.ready = body.ready ?? false;
2001
+ this.#queueDirectory();
2002
+ break;
2003
+ case FrameType.Exit:
2004
+ this.#exit(spawn, body.code ?? null);
2005
+ break;
2006
+ case FrameType.Error: spawn.events.error(new Error(body.message));
2007
+ }
2008
+ }
2009
+ #lost(proxy) {
2010
+ proxy.connection = null;
2011
+ const grace = this.options.reconnectGrace ?? 3e4;
2012
+ for (const spawn of proxy.spawns) {
2013
+ spawn.grace = setTimeout(() => this.#exit(spawn, null), grace);
2014
+ spawn.grace.unref();
2015
+ }
2016
+ if (proxy.spawns.size === 0) this.#proxies.delete(proxy.name);
2017
+ this.#queueDirectory();
2018
+ }
2019
+ #queueDirectory() {
2020
+ if (this.#directoryQueued) return;
2021
+ this.#directoryQueued = true;
2022
+ queueMicrotask(() => {
2023
+ this.#directoryQueued = false;
2024
+ this.#sendDirectory();
2025
+ });
2026
+ }
2027
+ #sendDirectory() {
2028
+ const byChannel = /* @__PURE__ */ new Map();
2029
+ for (const spawn of this.#spawns.values()) {
2030
+ if (!spawn.ready || !spawn.proxy?.connection || !spawn.proxy.peer) continue;
2031
+ const current = byChannel.get(spawn.context.id);
2032
+ if (!current || current.id < spawn.id) byChannel.set(spawn.context.id, spawn);
2033
+ }
2034
+ const directory = { entries: [...byChannel.values()].map((spawn) => ({
2035
+ channel: spawn.context.id,
2036
+ proxy: spawn.proxy.name,
2037
+ host: spawn.proxy.peer.host,
2038
+ port: spawn.proxy.peer.port
2039
+ })) };
2040
+ for (const proxy of this.#proxies.values()) if (proxy.connection?.closed === false && proxy.peer) proxy.connection.sendJson(FrameType.Directory, directory).catch(() => void 0);
2041
+ }
2042
+ };
2043
+
2044
+ //#endregion
2045
+ //#region src/strategies/WorkerStrategy.ts
2046
+ /**
2047
+ * Spawns every shard as a worker thread of the manager's process. Messages are the fastest, but the shards share the
2048
+ * process: a crash of the process stops them all.
2049
+ */
2050
+ var WorkerStrategy = class {
2051
+ name = "worker";
2052
+ options;
2053
+ constructor(options) {
2054
+ this.options = options;
2055
+ }
2056
+ spawn(context, events, options) {
2057
+ const worker = new Worker(this.options.path, {
2058
+ name: `shard ${context.id}`,
2059
+ ...this.options.worker,
2060
+ env: {
2061
+ ...process.env,
2062
+ ...options.env
2063
+ },
2064
+ workerData: { [ShardContextVariable]: encodeContext({
2065
+ ...context,
2066
+ transport: "worker"
2067
+ }) }
2068
+ });
2069
+ worker.on("message", (data) => events.message(data));
2070
+ worker.on("error", (error) => events.error(error));
2071
+ const exit = new Promise((resolve) => {
2072
+ worker.once("exit", (code) => {
2073
+ events.exit(code);
2074
+ resolve();
2075
+ });
2076
+ });
2077
+ return {
2078
+ pid: process.pid,
2079
+ threadId: worker.threadId,
2080
+ send: async (data) => {
2081
+ worker.postMessage(data);
2082
+ },
2083
+ kill: async () => {
2084
+ await worker.terminate();
2085
+ await exit;
2086
+ }
2087
+ };
2088
+ }
2089
+ };
2090
+
2091
+ //#endregion
2092
+ //#region src/strategies/registry.ts
2093
+ const strategies = /* @__PURE__ */ new Map([
2094
+ ["fork", (options) => new ForkStrategy(options)],
2095
+ ["cluster", (options) => new ClusterStrategy(options)],
2096
+ ["worker", (options) => new WorkerStrategy(options)],
2097
+ ["network", (options) => new NetworkStrategy(options)]
2098
+ ]);
2099
+ /**
2100
+ * Registers a strategy under a name, so managers and proxies can be given its name, like the built-in `"fork"`,
2101
+ * `"cluster"`, `"worker"`, and `"network"`.
2102
+ *
2103
+ * @param name The name of the strategy.
2104
+ * @param factory Builds the strategy from the `strategyOptions`.
2105
+ */
2106
+ function registerStrategy(name, factory) {
2107
+ strategies.set(name, factory);
2108
+ }
2109
+ /**
2110
+ * Resolves a strategy, or the name of a registered one.
2111
+ *
2112
+ * @param strategy The strategy, or its name.
2113
+ * @param options The options of a named strategy.
2114
+ */
2115
+ function resolveStrategy(strategy, options) {
2116
+ if (typeof strategy !== "string") return strategy;
2117
+ const factory = strategies.get(strategy);
2118
+ if (!factory) throw new RangeError(`There is no strategy named "${strategy}"`);
2119
+ return factory(options ?? {});
2120
+ }
2121
+
2122
+ //#endregion
2123
+ //#region src/util/gateway.ts
2124
+ /**
2125
+ * Fetches `GET /gateway/bot`.
2126
+ *
2127
+ * @param token The bot token.
2128
+ */
2129
+ async function fetchGatewayInformation(token) {
2130
+ const response = await fetch("https://discord.com/api/v10/gateway/bot", { headers: { Authorization: `Bot ${token.replace(/^Bot\s*/i, "")}` } });
2131
+ if (!response.ok) throw new Error(`Could not fetch the gateway information: ${response.status} ${response.statusText}`);
2132
+ return await response.json();
2133
+ }
2134
+ /**
2135
+ * Turns Discord's recommended shard count into the one to spawn.
2136
+ *
2137
+ * @param recommended The `shards` of `GET /gateway/bot`.
2138
+ * @param options How many guilds per gateway shard, and what to round the count up to.
2139
+ */
2140
+ function resolveRecommendedShardCount(recommended, options = {}) {
2141
+ const { guildsPerShard = 1e3, multipleOf = 1 } = options;
2142
+ return Math.ceil(recommended * (1e3 / guildsPerShard) / multipleOf) * multipleOf;
2143
+ }
2144
+ /**
2145
+ * Fetches how many gateway shards Discord recommends for the bot, like discord.js's `fetchRecommendedShardCount`.
2146
+ *
2147
+ * @param token The bot token.
2148
+ * @param options How many guilds per gateway shard, and what to round the count up to.
2149
+ */
2150
+ async function fetchRecommendedShardCount(token, options) {
2151
+ const { shards } = await fetchGatewayInformation(token);
2152
+ return resolveRecommendedShardCount(shards, options);
2153
+ }
2154
+ /**
2155
+ * Gets the gateway shard receiving the events of a guild.
2156
+ *
2157
+ * @param guildId The ID of the guild.
2158
+ * @param shardCount The total number of gateway shards.
2159
+ */
2160
+ function shardIdForGuild(guildId, shardCount) {
2161
+ return Number((BigInt(guildId) >> 22n) % BigInt(shardCount));
2162
+ }
2163
+ /**
2164
+ * Caches `GET /gateway/bot`, so the shards share one request instead of each sending theirs, and keeps its session
2165
+ * start limit up to date with the identifies it grants.
2166
+ *
2167
+ * @internal
2168
+ */
2169
+ var GatewayInformationCache = class {
2170
+ #fetch;
2171
+ #ttl;
2172
+ #value = null;
2173
+ #fetchedAt = 0;
2174
+ #resetAt = 0;
2175
+ #pending = null;
2176
+ constructor(fetcher, ttl) {
2177
+ this.#fetch = fetcher;
2178
+ this.#ttl = ttl;
2179
+ }
2180
+ async get(force = false) {
2181
+ const now = Date.now();
2182
+ if (!force && this.#value && now - this.#fetchedAt < this.#ttl) return this.#current(now);
2183
+ this.#pending ??= this.#fetch().then((value) => {
2184
+ this.#value = structuredClone(value);
2185
+ this.#fetchedAt = Date.now();
2186
+ this.#resetAt = this.#fetchedAt + value.session_start_limit.reset_after;
2187
+ return value;
2188
+ }).finally(() => {
2189
+ this.#pending = null;
2190
+ });
2191
+ await this.#pending;
2192
+ return this.#current(Date.now());
2193
+ }
2194
+ /**
2195
+ * Counts an identify against the session start limit.
2196
+ */
2197
+ consume() {
2198
+ if (!this.#value) return;
2199
+ this.#current(Date.now());
2200
+ this.#value.session_start_limit.remaining = Math.max(this.#value.session_start_limit.remaining - 1, 0);
2201
+ }
2202
+ #current(now) {
2203
+ const value = this.#value;
2204
+ const limit = value.session_start_limit;
2205
+ if (now >= this.#resetAt) {
2206
+ limit.remaining = limit.total;
2207
+ this.#resetAt = now + 864e5;
2208
+ }
2209
+ limit.reset_after = Math.max(this.#resetAt - now, 0);
2210
+ return structuredClone(value);
2211
+ }
2212
+ };
2213
+
2214
+ //#endregion
2215
+ //#region src/util/IdentifyQueue.ts
2216
+ /**
2217
+ * Hands out the turns of gateway shards to identify, across every process: Discord allows one identify per
2218
+ * `max_concurrency` bucket (`shardId % max_concurrency`) every 5 seconds.
2219
+ *
2220
+ * @internal
2221
+ */
2222
+ var IdentifyQueue = class {
2223
+ #delay;
2224
+ #buckets = /* @__PURE__ */ new Map();
2225
+ #concurrency;
2226
+ constructor(concurrency, delay) {
2227
+ this.#concurrency = concurrency;
2228
+ this.#delay = delay;
2229
+ }
2230
+ set concurrency(value) {
2231
+ this.#concurrency = Math.max(1, value);
2232
+ }
2233
+ get concurrency() {
2234
+ return this.#concurrency;
2235
+ }
2236
+ /**
2237
+ * Resolves once the gateway shard may identify. Aborting it drops it from the queue.
2238
+ *
2239
+ * @param shardId The ID of the gateway shard.
2240
+ * @param signal Aborts the wait.
2241
+ */
2242
+ wait(shardId, signal) {
2243
+ const bucket = shardId % this.#concurrency;
2244
+ const previous = this.#buckets.get(bucket) ?? Promise.resolve();
2245
+ let release;
2246
+ const released = new Promise((resolve) => {
2247
+ release = resolve;
2248
+ });
2249
+ this.#buckets.set(bucket, previous.then(() => released));
2250
+ return new Promise((resolve, reject) => {
2251
+ const onAbort = () => {
2252
+ release();
2253
+ reject(signal.reason);
2254
+ };
2255
+ signal?.addEventListener("abort", onAbort, { once: true });
2256
+ previous.then(() => {
2257
+ signal?.removeEventListener("abort", onAbort);
2258
+ if (signal?.aborted) return;
2259
+ resolve();
2260
+ setTimeout$1(this.#delay, void 0, { ref: false }).then(release);
2261
+ });
2262
+ });
2263
+ }
2264
+ };
2265
+
2266
+ //#endregion
2267
+ //#region src/util/Supervisor.ts
2268
+ /**
2269
+ * Counts the crashes of each shard against the supervisor's intensity.
2270
+ *
2271
+ * @internal
2272
+ */
2273
+ var Supervisor = class {
2274
+ intensity;
2275
+ period;
2276
+ strategy;
2277
+ #crashes = /* @__PURE__ */ new Map();
2278
+ constructor(options = {}) {
2279
+ const { intensity = -1, period = 0, strategy = "one-for-one" } = options;
2280
+ if (intensity !== -1 && intensity !== Number.POSITIVE_INFINITY) {
2281
+ if (!Number.isSafeInteger(intensity) || intensity < 0) throw new RangeError("supervisor.intensity must be a non-negative integer, -1, or Infinity");
2282
+ }
2283
+ this.intensity = intensity === Number.POSITIVE_INFINITY ? -1 : intensity;
2284
+ this.period = period;
2285
+ this.strategy = strategy;
2286
+ }
2287
+ /**
2288
+ * Counts a crash. Returns how many crashes count, and whether the shard may be restarted.
2289
+ *
2290
+ * @param channelId The ID of the crashed shard.
2291
+ */
2292
+ crash(channelId) {
2293
+ const now = Date.now();
2294
+ const crashes = (this.#crashes.get(channelId) ?? []).filter((timestamp) => this.period === 0 || now - timestamp < this.period);
2295
+ crashes.push(now);
2296
+ this.#crashes.set(channelId, crashes);
2297
+ return {
2298
+ crashes: crashes.length,
2299
+ restart: this.intensity === -1 || crashes.length <= this.intensity
2300
+ };
2301
+ }
2302
+ /**
2303
+ * Forgets the crashes of a shard that is ready again, when they are counted in a row.
2304
+ *
2305
+ * @param channelId The ID of the shard.
2306
+ */
2307
+ ready(channelId) {
2308
+ if (this.period === 0) this.#crashes.delete(channelId);
2309
+ }
2310
+ };
2311
+
2312
+ //#endregion
2313
+ //#region src/ShardManager.ts
2314
+ /**
2315
+ * Spawns shards, keeps them alive, and carries messages between them.
2316
+ *
2317
+ * @remarks
2318
+ * Follows discord.js's sharder RFC (discordjs/discord.js#8084): a "shard" is a process, cluster worker, worker
2319
+ * thread, or remote process, which may connect several gateway shards, and the manager talks to each through a
2320
+ * {@link ShardChannel}. The manager is agnostic of the bot: a shard runs any script using {@link ShardClient}.
2321
+ *
2322
+ * @example
2323
+ * ```ts
2324
+ * const manager = new ShardManager({ strategy: new ForkStrategy({ path: "./bot.js" }), clusters: 4 });
2325
+ * manager.setRequestHandler((body, { channel }) => ...);
2326
+ * await manager.spawn();
2327
+ * const guilds = await manager.broadcastRequest({ type: "guildCount" });
2328
+ * ```
2329
+ */
2330
+ var ShardManager = class extends EventEmitter {
2331
+ strategy;
2332
+ /**
2333
+ * The channels to the shards, by ID. Empty until {@link ShardManager.spawn} for `"auto"` layouts.
2334
+ */
2335
+ channels = [];
2336
+ /**
2337
+ * The total number of gateway shards, `0` until resolved for `"auto"` layouts.
2338
+ */
2339
+ shardCount = 0;
2340
+ spawnDelay;
2341
+ spawnTimeout;
2342
+ readyHint;
2343
+ readyHintMargin;
2344
+ requestTimeout;
2345
+ pingOptions;
2346
+ /**
2347
+ * @internal
2348
+ */
2349
+ codec;
2350
+ /**
2351
+ * @internal
2352
+ */
2353
+ supervisor;
2354
+ /**
2355
+ * @internal
2356
+ */
2357
+ spawnEnv;
2358
+ /**
2359
+ * @internal
2360
+ */
2361
+ requestHandler = null;
2362
+ #layout;
2363
+ #token;
2364
+ #gateway;
2365
+ #identify;
2366
+ #identifyConcurrency;
2367
+ #readyTimes = [];
2368
+ #queue = Promise.resolve();
2369
+ #initialized = false;
2370
+ constructor(options = {}) {
2371
+ super();
2372
+ this.strategy = resolveStrategy(options.strategy ?? "fork", options.strategyOptions);
2373
+ this.supervisor = new Supervisor(options.supervisor);
2374
+ this.spawnDelay = options.spawn?.delay ?? 5e3;
2375
+ this.spawnTimeout = options.spawn?.timeout ?? 3e4;
2376
+ this.readyHint = options.spawn?.readyHint ?? null;
2377
+ this.readyHintMargin = options.spawn?.readyHintMargin ?? .1;
2378
+ this.pingOptions = {
2379
+ interval: options.ping?.interval ?? 45e3,
2380
+ timeout: options.ping?.timeout ?? 6e4,
2381
+ delaySinceReceived: options.ping?.delaySinceReceived ?? false
2382
+ };
2383
+ this.requestTimeout = options.requestTimeout ?? this.pingOptions.timeout;
2384
+ this.codec = new PacketCodec(resolveMessageHandler(options.messageHandler ?? "json"), (options.transformers ?? []).map(resolveMessageTransformer));
2385
+ if (this.codec.handler.name === "raw" && this.codec.transformers.length > 0) throw new TypeError("The raw message handler cannot be combined with transformers");
2386
+ this.#token = options.token ?? process.env.DISCORD_TOKEN ?? null;
2387
+ this.spawnEnv = options.token ? { DISCORD_TOKEN: options.token } : {};
2388
+ const fetcher = options.gatewayInformation?.fetch ?? (this.#token ? () => fetchGatewayInformation(this.#token) : null);
2389
+ this.#gateway = fetcher ? new GatewayInformationCache(fetcher, options.gatewayInformation?.ttl ?? 864e5) : null;
2390
+ this.#identifyConcurrency = options.identify?.concurrency ?? "auto";
2391
+ this.#identify = new IdentifyQueue(this.#identifyConcurrency === "auto" ? 1 : this.#identifyConcurrency, options.identify?.delay ?? 5e3);
2392
+ this.#layout = {
2393
+ shards: options.shards,
2394
+ totalShards: options.totalShards,
2395
+ shardList: options.shardList,
2396
+ clusters: options.clusters,
2397
+ recommended: options.recommended
2398
+ };
2399
+ if (!needsGateway(this.#layout)) this.#apply(resolveLayout(this.#layout, null));
2400
+ }
2401
+ /**
2402
+ * How long a shard is expected to take to be ready: `spawn.readyHint`, or the average of the shards so far.
2403
+ */
2404
+ get readyEstimate() {
2405
+ if (this.readyHint !== null) return this.readyHint;
2406
+ if (this.#readyTimes.length === 0) return null;
2407
+ return this.#readyTimes.reduce((sum, time) => sum + time, 0) / this.#readyTimes.length;
2408
+ }
2409
+ /**
2410
+ * Spawns every shard, one after the other, each waiting for the previous one to be ready plus the spawn delay. A
2411
+ * shard not ready in time is killed and tried again at the end of the queue, as long as the supervisor allows.
2412
+ */
2413
+ async spawn() {
2414
+ if (this.channels.length === 0) {
2415
+ const info = needsGateway(this.#layout) ? await this.fetchGatewayInformation() : null;
2416
+ this.#apply(resolveLayout(this.#layout, info));
2417
+ }
2418
+ await this.#init();
2419
+ await Promise.all(this.channels.map((channel) => this.#startWithRetries(channel)));
2420
+ }
2421
+ /**
2422
+ * Fetches `GET /gateway/bot`, cached for every shard, with its session start limit kept up to date.
2423
+ *
2424
+ * @param force Whether to skip the cache.
2425
+ */
2426
+ async fetchGatewayInformation(force = false) {
2427
+ if (!this.#gateway) throw new Error("Fetching the gateway information needs a token, or gatewayInformation.fetch");
2428
+ return this.#gateway.get(force);
2429
+ }
2430
+ /**
2431
+ * Gets the channel to the shard connecting a gateway shard.
2432
+ *
2433
+ * @param shardId The ID of the gateway shard.
2434
+ */
2435
+ channelFor(shardId) {
2436
+ return this.channels.find((channel) => channel.shards.includes(shardId));
2437
+ }
2438
+ /**
2439
+ * Gets the channel to the shard receiving the events of a guild.
2440
+ *
2441
+ * @param guildId The ID of the guild.
2442
+ */
2443
+ channelForGuild(guildId) {
2444
+ return this.channelFor(shardIdForGuild(guildId, this.shardCount));
2445
+ }
2446
+ /**
2447
+ * Sets the handler answering the requests the shards send to the manager.
2448
+ *
2449
+ * @param handler The handler; its return value is the reply.
2450
+ */
2451
+ setRequestHandler(handler) {
2452
+ this.requestHandler = handler;
2453
+ return this;
2454
+ }
2455
+ /**
2456
+ * Sends a message to a shard, emitted as `message` by its {@link ShardClient}.
2457
+ *
2458
+ * @param channelId The ID of the shard.
2459
+ * @param body The message.
2460
+ * @param options How long to wait for the shard to be ready, and an abort signal.
2461
+ */
2462
+ send(channelId, body, options) {
2463
+ return this.#channel(channelId).send(body, options);
2464
+ }
2465
+ /**
2466
+ * Sends a request to a shard, answered by its {@link ShardClient}'s request handler.
2467
+ *
2468
+ * @param channelId The ID of the shard.
2469
+ * @param body The request.
2470
+ * @param options The timeout and abort signal of the request.
2471
+ */
2472
+ request(channelId, body, options) {
2473
+ return this.#channel(channelId).request(body, options);
2474
+ }
2475
+ /**
2476
+ * Sends a message to every shard.
2477
+ *
2478
+ * @param body The message.
2479
+ * @param options How long to wait for the shards to be ready, and an abort signal.
2480
+ */
2481
+ async broadcast(body, options) {
2482
+ await Promise.all(this.channels.map((channel) => channel.send(body, options)));
2483
+ }
2484
+ broadcastRequest(body, options = {}) {
2485
+ const requests = this.channels.map((channel) => channel.request(body, options));
2486
+ return options.partial ? Promise.allSettled(requests) : Promise.all(requests);
2487
+ }
2488
+ /**
2489
+ * {@link ShardManager.send}, resolving with a `Result` rather than rejecting.
2490
+ */
2491
+ trySend(channelId, body, options) {
2492
+ return toResult(() => this.send(channelId, body, options));
2493
+ }
2494
+ /**
2495
+ * {@link ShardManager.request}, resolving with a `Result` rather than rejecting.
2496
+ */
2497
+ tryRequest(channelId, body, options) {
2498
+ return toResult(() => this.request(channelId, body, options));
2499
+ }
2500
+ /**
2501
+ * {@link ShardManager.broadcastRequest}, resolving with one `Result` per shard, by shard ID.
2502
+ */
2503
+ async tryBroadcastRequest(body, options) {
2504
+ return settledToResults(await this.broadcastRequest(body, {
2505
+ ...options,
2506
+ partial: true
2507
+ }));
2508
+ }
2509
+ /**
2510
+ * Restarts a shard: closes it then spawns it again, or with `rolling`, spawns the new one first and closes the old
2511
+ * one once the new one is ready.
2512
+ *
2513
+ * @param channelId The ID of the shard.
2514
+ * @param options Whether to restart it rolling, and the timeout.
2515
+ */
2516
+ async restart(channelId, options = {}) {
2517
+ const channel = this.#channel(channelId);
2518
+ this.emit("shardRestart", channel);
2519
+ if (options.rolling) {
2520
+ await this.#enqueue(() => channel.rollingRestart(options.timeout));
2521
+ return;
2522
+ }
2523
+ await channel.close(options.timeout);
2524
+ await this.#startWithRetries(channel, true);
2525
+ }
2526
+ /**
2527
+ * Restarts every shard, one after the other, with the spawn delay between them.
2528
+ *
2529
+ * @param options Whether to restart them rolling, and the timeout.
2530
+ */
2531
+ async restartAll(options) {
2532
+ for (const channel of this.channels) await this.restart(channel.id, options);
2533
+ }
2534
+ /**
2535
+ * Reshards with close to no downtime: spawns the shards of a new layout while the current ones keep running, then
2536
+ * closes the current ones once every new one is ready.
2537
+ *
2538
+ * @param layout The new layout. Left out, Discord's recommendation split across `clusters`.
2539
+ */
2540
+ async reshard(layout = { shards: "auto" }) {
2541
+ const resolved = resolveLayout(layout, needsGateway(layout) ? await this.fetchGatewayInformation(true) : null);
2542
+ const channels = resolved.channels.map((shards, id) => new ShardChannel(this, id, shards, resolved.shardCount));
2543
+ try {
2544
+ await Promise.all(channels.map((channel) => this.#startWithRetries(channel)));
2545
+ } catch (error) {
2546
+ await Promise.all(channels.map((channel) => channel.close()));
2547
+ throw error;
2548
+ }
2549
+ const previous = this.channels;
2550
+ this.channels = channels;
2551
+ this.shardCount = resolved.shardCount;
2552
+ await Promise.all(previous.map((channel) => channel.close()));
2553
+ }
2554
+ /**
2555
+ * Asks the shard connecting a gateway shard to start it, through its {@link ShardClient.setShardHandler}.
2556
+ *
2557
+ * @param shardId The ID of the gateway shard.
2558
+ * @param options The timeout and abort signal of the request.
2559
+ */
2560
+ startShard(shardId, options) {
2561
+ return this.#channelForShard(shardId).startShard(shardId, options);
2562
+ }
2563
+ /**
2564
+ * Asks the shard connecting a gateway shard to close it, through its {@link ShardClient.setShardHandler}.
2565
+ *
2566
+ * @param shardId The ID of the gateway shard.
2567
+ * @param options The timeout and abort signal of the request.
2568
+ */
2569
+ closeShard(shardId, options) {
2570
+ return this.#channelForShard(shardId).closeShard(shardId, options);
2571
+ }
2572
+ /**
2573
+ * Closes then starts a gateway shard, see {@link ShardManager.startShard}.
2574
+ *
2575
+ * @param shardId The ID of the gateway shard.
2576
+ * @param options The timeout and abort signal of each request.
2577
+ */
2578
+ async restartShard(shardId, options) {
2579
+ await this.closeShard(shardId, options);
2580
+ await this.startShard(shardId, options);
2581
+ }
2582
+ /**
2583
+ * Waits for a gateway shard's turn to identify, like {@link ShardClient.identifyThrottler} does from a shard.
2584
+ *
2585
+ * @param shardId The ID of the gateway shard.
2586
+ * @param signal Aborts the wait.
2587
+ */
2588
+ async waitForIdentify(shardId, signal) {
2589
+ if (this.#identifyConcurrency === "auto" && this.#gateway) {
2590
+ const info = await this.fetchGatewayInformation();
2591
+ this.#identify.concurrency = info.session_start_limit.max_concurrency;
2592
+ }
2593
+ await this.#identify.wait(shardId, signal);
2594
+ this.#gateway?.consume();
2595
+ }
2596
+ /**
2597
+ * Closes every shard for good, and releases the strategy.
2598
+ */
2599
+ async destroy() {
2600
+ await Promise.all(this.channels.map((channel) => channel.close()));
2601
+ if (this.#initialized) await this.strategy.destroy?.();
2602
+ this.#initialized = false;
2603
+ }
2604
+ /**
2605
+ * Carries a message a shard sends to another shard, or to every shard.
2606
+ *
2607
+ * @internal
2608
+ */
2609
+ async route(body, to, from) {
2610
+ if (to === "all") await Promise.all(this.channels.map((channel) => channel.send(body, {}, from)));
2611
+ else await this.#channel(to).send(body, {}, from);
2612
+ }
2613
+ /**
2614
+ * Carries a request a shard sends to another shard, or to every shard.
2615
+ *
2616
+ * @internal
2617
+ */
2618
+ async forward(body, to, from, options) {
2619
+ const forwarded = {
2620
+ timeout: options.timeout === void 0 ? void 0 : Math.max(Math.floor(options.timeout * .95), 1),
2621
+ signal: options.signal
2622
+ };
2623
+ if (to !== "all") return this.#channel(to).request(body, forwarded, from);
2624
+ const requests = this.channels.map((channel) => channel.request(body, forwarded, from));
2625
+ return options.partial ? serializeSettled(await Promise.allSettled(requests)) : Promise.all(requests);
2626
+ }
2627
+ /**
2628
+ * Answers the requests the sharder itself sends.
2629
+ *
2630
+ * @internal
2631
+ */
2632
+ async handleSystem(call, body, channel, signal) {
2633
+ switch (call) {
2634
+ case "identify":
2635
+ await this.waitForIdentify(body, signal);
2636
+ return null;
2637
+ case "gatewayInformation": return this.fetchGatewayInformation();
2638
+ case "control": return this.#control(body, channel);
2639
+ default: throw new Error(`Shards cannot send the ${call} system request`);
2640
+ }
2641
+ }
2642
+ /**
2643
+ * Decides what happens to a shard that stopped on its own.
2644
+ *
2645
+ * @internal
2646
+ */
2647
+ supervise(channel, previous) {
2648
+ if (previous === "Exiting") {
2649
+ channel.markStopped("it exited");
2650
+ return;
2651
+ }
2652
+ if (previous !== "Restarting") {
2653
+ const { crashes, restart } = this.supervisor.crash(channel.id);
2654
+ if (!restart) {
2655
+ channel.markStopped("it crashed too often");
2656
+ if (this.listenerCount("shardGiveUp") > 0) this.emit("shardGiveUp", channel, crashes);
2657
+ else this.reportError(new ShardUnavailableError(channel.id, `it crashed ${crashes} times`));
2658
+ return;
2659
+ }
2660
+ const others = this.supervisor.strategy === "one-for-all" ? this.channels.filter((other) => other !== channel) : this.supervisor.strategy === "rest-for-one" ? this.channels.filter((other) => other.id > channel.id) : [];
2661
+ for (const other of others) if (!other.stopped) this.restart(other.id).catch((error) => this.reportError(error));
2662
+ }
2663
+ this.emit("shardRestart", channel);
2664
+ this.#startWithRetries(channel).catch((error) => this.reportError(error));
2665
+ }
2666
+ /**
2667
+ * @internal
2668
+ */
2669
+ recordReadyTime(time) {
2670
+ this.#readyTimes.push(time);
2671
+ if (this.#readyTimes.length > 20) this.#readyTimes.shift();
2672
+ }
2673
+ /**
2674
+ * @internal
2675
+ */
2676
+ reportError(error) {
2677
+ if (this.listenerCount("error") > 0) this.emit("error", error);
2678
+ else console.error(error);
2679
+ }
2680
+ /**
2681
+ * @internal
2682
+ */
2683
+ reportShardError(channel, error) {
2684
+ if (channel.listenerCount("error") > 0) channel.emit("error", error);
2685
+ if (this.listenerCount("shardError") > 0) this.emit("shardError", channel, error);
2686
+ else if (channel.listenerCount("error") === 0) this.reportError(error);
2687
+ }
2688
+ /**
2689
+ * @internal
2690
+ */
2691
+ reportInvalidMessage(channel, error) {
2692
+ if (this.listenerCount("shardInvalidMessage") > 0) this.emit("shardInvalidMessage", channel, error);
2693
+ else console.error(`Shard ${channel.id} sent an invalid message:`, error);
2694
+ }
2695
+ async #control(request, from) {
2696
+ const { action, target } = request;
2697
+ if ("shard" in target) {
2698
+ if (action === "start") await this.startShard(target.shard);
2699
+ else if (action === "close") await this.closeShard(target.shard);
2700
+ else await this.restartShard(target.shard);
2701
+ return null;
2702
+ }
2703
+ const channels = target.channel === "all" ? this.channels : [this.#channel(target.channel)];
2704
+ const run = async () => {
2705
+ for (const channel of channels) if (action === "restart") await this.restart(channel.id);
2706
+ else if (action === "close") await channel.close();
2707
+ else if (!channel.running) await this.#startWithRetries(channel, true);
2708
+ };
2709
+ if (action !== "start" && channels.includes(from)) setImmediate(() => void run().catch((error) => this.reportError(error)));
2710
+ else await run();
2711
+ return null;
2712
+ }
2713
+ async #init() {
2714
+ if (this.#initialized) return;
2715
+ await this.strategy.init?.();
2716
+ this.#initialized = true;
2717
+ }
2718
+ async #startWithRetries(channel, force = false) {
2719
+ await this.#init();
2720
+ for (;;) try {
2721
+ await this.#enqueue(async () => {
2722
+ if (!force && channel.stopped || channel.running) return;
2723
+ await channel.start();
2724
+ });
2725
+ return;
2726
+ } catch (error) {
2727
+ if (channel.stopped) throw error;
2728
+ const { restart } = this.supervisor.crash(channel.id);
2729
+ if (!restart) {
2730
+ channel.markStopped("it failed to start too often");
2731
+ throw error;
2732
+ }
2733
+ }
2734
+ }
2735
+ #enqueue(task) {
2736
+ const run = this.#queue.then(task);
2737
+ this.#queue = run.then(() => setTimeout$1(this.spawnDelay), () => setTimeout$1(this.spawnDelay));
2738
+ return run;
2739
+ }
2740
+ #apply(layout) {
2741
+ this.shardCount = layout.shardCount;
2742
+ this.channels = layout.channels.map((shards, id) => new ShardChannel(this, id, shards, layout.shardCount));
2743
+ }
2744
+ #channel(channelId) {
2745
+ const channel = this.channels[channelId];
2746
+ if (!channel) throw new ShardUnavailableError(channelId, "there is no such shard");
2747
+ return channel;
2748
+ }
2749
+ #channelForShard(shardId) {
2750
+ const channel = this.channelFor(shardId);
2751
+ if (!channel) throw new RangeError(`No shard connects the gateway shard ${shardId}`);
2752
+ return channel;
2753
+ }
2754
+ };
2755
+ function needsGateway(layout) {
2756
+ return layout.totalShards === "auto" || layout.totalShards === void 0 && (layout.shards ?? "auto") === "auto" && !layout.shardList;
2757
+ }
2758
+ function resolveLayout(layout, info) {
2759
+ const { shards = "auto", shardList, clusters = availableParallelism() } = layout;
2760
+ const recommended = info ? resolveRecommendedShardCount(info.shards, layout.recommended) : null;
2761
+ const sizes = Array.isArray(shards) ? shards : null;
2762
+ if (sizes?.some((size) => !Number.isSafeInteger(size) || size < 1) || sizes?.length === 0) throw new RangeError("shards must list positive integers");
2763
+ const declared = typeof shards === "number" ? shards : sizes ? sizes.reduce((sum, size) => sum + size, 0) : null;
2764
+ const totalShards = layout.totalShards === "auto" ? recommended : layout.totalShards ?? declared ?? shardList?.length ?? recommended;
2765
+ if (!Number.isSafeInteger(totalShards) || totalShards < 1) throw new RangeError("The total number of gateway shards must be a positive integer");
2766
+ const ids = shardList ? [...shardList] : range(declared ?? totalShards);
2767
+ if (new Set(ids).size !== ids.length || ids.some((id) => id < 0 || id >= totalShards)) throw new RangeError(`shardList must list distinct gateway shards below ${totalShards}`);
2768
+ let channels;
2769
+ if (sizes) {
2770
+ if (ids.length !== declared) throw new RangeError("shards must add up to the shardList's length");
2771
+ let offset = 0;
2772
+ channels = sizes.map((size) => ids.slice(offset, offset += size));
2773
+ } else if (typeof shards === "number") {
2774
+ if (ids.length !== shards) throw new RangeError("shards must be the shardList's length");
2775
+ channels = ids.map((id) => [id]);
2776
+ } else {
2777
+ if (!Number.isSafeInteger(clusters) || clusters < 1) throw new RangeError("clusters must be a positive integer");
2778
+ const size = Math.ceil(ids.length / clusters);
2779
+ channels = [];
2780
+ for (let index = 0; index < ids.length; index += size) channels.push(ids.slice(index, index + size));
2781
+ }
2782
+ return {
2783
+ shardCount: totalShards,
2784
+ channels
2785
+ };
2786
+ }
2787
+ function range(length) {
2788
+ return Array.from({ length }, (_, index) => index);
2789
+ }
2790
+
2791
+ //#endregion
2792
+ //#region src/ShardManagerProxy.ts
2793
+ /**
2794
+ * Runs shards for remote {@link ShardManager}s using a {@link NetworkStrategy}: the sharder RFC's
2795
+ * `ShardManagerProxy`. It spawns them locally with its own strategy, carries their messages to their manager, and
2796
+ * carries the messages between shards of the same manager directly, locally or through peer proxies.
2797
+ *
2798
+ * @example
2799
+ * ```ts
2800
+ * const proxy = new ShardManagerProxy({
2801
+ * managers: ["manager.internal:7000", "manager-backup.internal:7000"],
2802
+ * token: process.env.SHARDER_TOKEN!,
2803
+ * tls: { ca: readFileSync("ca.pem") },
2804
+ * strategy: new ForkStrategy({ path: "./bot.js" }),
2805
+ * capacity: 8,
2806
+ * peer: { port: 7001 },
2807
+ * });
2808
+ * await proxy.connect();
2809
+ * ```
2810
+ */
2811
+ var ShardManagerProxy = class extends EventEmitter {
2812
+ name;
2813
+ capacity;
2814
+ strategy;
2815
+ managers;
2816
+ mode;
2817
+ #options;
2818
+ #links;
2819
+ #shards = /* @__PURE__ */ new Map();
2820
+ #byChannel = /* @__PURE__ */ new Map();
2821
+ #routes = /* @__PURE__ */ new Map();
2822
+ #peerRequests = /* @__PURE__ */ new Map();
2823
+ #peers = /* @__PURE__ */ new Map();
2824
+ #connecting = /* @__PURE__ */ new Set();
2825
+ #inbound = /* @__PURE__ */ new Set();
2826
+ #codecs = /* @__PURE__ */ new Map();
2827
+ #peerServer = null;
2828
+ #peerAddress = null;
2829
+ #destroyed = false;
2830
+ #nextNonce = 0;
2831
+ constructor(options) {
2832
+ super();
2833
+ if (options.managers.length === 0) throw new RangeError("A proxy needs at least one manager");
2834
+ if (options.peer && options.localRouting === false) throw new TypeError("Peer routing needs local routing");
2835
+ this.#options = options;
2836
+ this.name = options.name ?? `${hostname()}:${process.pid}`;
2837
+ this.capacity = options.capacity ?? availableParallelism();
2838
+ this.strategy = resolveStrategy(options.strategy ?? "fork", options.strategyOptions);
2839
+ this.managers = options.managers.map(parseAddress);
2840
+ this.mode = options.mode ?? "failover";
2841
+ this.#links = this.mode === "all" ? this.managers.map((address, index) => createLink([address], index)) : [createLink(this.managers, 0)];
2842
+ }
2843
+ /**
2844
+ * The managers the proxy is connected to.
2845
+ */
2846
+ get connectedManagers() {
2847
+ return this.#links.flatMap((link) => link.connection && link.address ? [link.address] : []);
2848
+ }
2849
+ /**
2850
+ * The manager the proxy is connected to in `"failover"` mode, `null` while it is not.
2851
+ */
2852
+ get manager() {
2853
+ return this.connectedManagers[0] ?? null;
2854
+ }
2855
+ /**
2856
+ * The contexts of the shards the proxy runs.
2857
+ */
2858
+ get shards() {
2859
+ return [...this.#shards.values()].map((shard) => shard.context);
2860
+ }
2861
+ /**
2862
+ * Where other proxies reach this one, once connected with `peer`.
2863
+ */
2864
+ get peerAddress() {
2865
+ return this.#peerAddress;
2866
+ }
2867
+ /**
2868
+ * The names of the proxies this one has a peer connection with.
2869
+ */
2870
+ get peers() {
2871
+ return [...new Set([...this.#peers.values(), ...this.#inbound].map((peer) => peer.name))];
2872
+ }
2873
+ /**
2874
+ * Connects to the managers, and keeps reconnecting whenever a connection is lost, until
2875
+ * {@link ShardManagerProxy.destroy}. Resolves once connected to a first manager.
2876
+ */
2877
+ async connect() {
2878
+ await this.strategy.init?.();
2879
+ await this.#listenPeers();
2880
+ await new Promise((resolve) => {
2881
+ this.once("connect", () => resolve());
2882
+ for (const link of this.#links) this.#loop(link);
2883
+ });
2884
+ }
2885
+ /**
2886
+ * Stops every shard, and disconnects for good.
2887
+ */
2888
+ async destroy() {
2889
+ this.#destroyed = true;
2890
+ for (const link of this.#links) link.connection?.close();
2891
+ await Promise.all([...this.#shards.values()].map((shard) => shard.transport.kill()));
2892
+ for (const peer of [...this.#peers.values(), ...this.#inbound]) peer.connection.close();
2893
+ const server = this.#peerServer;
2894
+ this.#peerServer = null;
2895
+ if (server) await new Promise((resolve) => server.close(() => resolve()));
2896
+ await this.strategy.destroy?.();
2897
+ }
2898
+ async #loop(link) {
2899
+ for (let attempt = 0; !this.#destroyed; ++attempt) {
2900
+ const address = link.addresses[attempt % link.addresses.length];
2901
+ const error = await this.#session(link, address);
2902
+ if (this.#destroyed) return;
2903
+ if (error !== void 0) this.emit("disconnect", address, error ?? void 0);
2904
+ await setTimeout$1(this.#options.reconnectDelay ?? 5e3);
2905
+ }
2906
+ }
2907
+ #session(link, address) {
2908
+ return new Promise((resolve) => {
2909
+ const tls = address.tls ?? this.#options.tls;
2910
+ const socket = tls ? connect$1({
2911
+ ...tls === true ? {} : tls,
2912
+ host: address.host,
2913
+ port: address.port
2914
+ }) : connect({
2915
+ host: address.host,
2916
+ port: address.port
2917
+ });
2918
+ socket.once("error", () => resolve(void 0));
2919
+ socket.once(tls ? "secureConnect" : "connect", () => {
2920
+ socket.removeAllListeners("error");
2921
+ const connection = new Connection(socket, this.#heartbeat());
2922
+ let welcomed = false;
2923
+ connection.on("frame", (type, payload) => {
2924
+ if (welcomed) this.#frame(link, type, payload);
2925
+ else if (type === FrameType.Welcome) {
2926
+ welcomed = true;
2927
+ this.#welcome(link, address, connection, JSON.parse(payload.toString()));
2928
+ } else if (type === FrameType.Reject) {
2929
+ const { reason } = JSON.parse(payload.toString());
2930
+ this.emit("reject", address, reason);
2931
+ connection.close();
2932
+ }
2933
+ });
2934
+ connection.on("close", (error) => {
2935
+ if (link.connection === connection) this.#lost(link);
2936
+ resolve(welcomed ? error ?? null : void 0);
2937
+ });
2938
+ const hello = {
2939
+ token: address.token ?? this.#options.token,
2940
+ name: this.name,
2941
+ capacity: this.capacity,
2942
+ available: this.#available(),
2943
+ running: [...this.#shards.values()].filter((shard) => shard.link === link).map((shard) => shard.spawnId),
2944
+ peer: this.#peerAddress
2945
+ };
2946
+ connection.sendJson(FrameType.Hello, hello).catch(() => void 0);
2947
+ });
2948
+ });
2949
+ }
2950
+ #welcome(link, address, connection, welcome) {
2951
+ const kill = new Set(welcome.kill);
2952
+ for (const shard of this.#shards.values()) if (shard.link === link && kill.has(shard.spawnId)) shard.transport.kill();
2953
+ link.connection = connection;
2954
+ link.address = address;
2955
+ link.managerId = welcome.managerId;
2956
+ this.emit("connect", address);
2957
+ }
2958
+ #lost(link) {
2959
+ link.connection = null;
2960
+ link.directory.clear();
2961
+ if (this.#options.managerLoss === "exit") {
2962
+ for (const shard of this.#shards.values()) if (shard.link === link) shard.transport.kill();
2963
+ }
2964
+ }
2965
+ #frame(link, type, payload) {
2966
+ switch (type) {
2967
+ case FrameType.StringData:
2968
+ case FrameType.BinaryData: {
2969
+ const { spawnId, data } = readData(type, payload);
2970
+ this.#shards.get(shardKey(link, spawnId))?.transport.send(data).catch(() => void 0);
2971
+ break;
2972
+ }
2973
+ case FrameType.Spawn:
2974
+ this.#spawn(link, JSON.parse(payload.toString()));
2975
+ break;
2976
+ case FrameType.Kill: {
2977
+ const { spawnId } = JSON.parse(payload.toString());
2978
+ const shard = this.#shards.get(shardKey(link, spawnId));
2979
+ if (shard) shard.transport.kill();
2980
+ else this.#report(link, FrameType.Exit, {
2981
+ spawnId,
2982
+ code: null
2983
+ });
2984
+ break;
2985
+ }
2986
+ case FrameType.Directory: {
2987
+ const { entries } = JSON.parse(payload.toString());
2988
+ link.directory = new Map(entries.map(({ channel, ...entry }) => [channel, entry]));
2989
+ break;
2990
+ }
2991
+ }
2992
+ }
2993
+ #spawn(link, frame) {
2994
+ const context = frame.context;
2995
+ const key = shardKey(link, frame.spawnId);
2996
+ if (this.#shards.size >= this.capacity) {
2997
+ this.#report(link, FrameType.Error, {
2998
+ spawnId: frame.spawnId,
2999
+ message: "The proxy is full"
3000
+ });
3001
+ this.#report(link, FrameType.Exit, {
3002
+ spawnId: frame.spawnId,
3003
+ code: null
3004
+ });
3005
+ return;
3006
+ }
3007
+ let shard = null;
3008
+ try {
3009
+ const transport = this.strategy.spawn(context, {
3010
+ message: (data) => void this.#fromShard(shard, data),
3011
+ exit: (code) => this.#exited(shard, code),
3012
+ error: (error) => {
3013
+ this.#report(link, FrameType.Error, {
3014
+ spawnId: frame.spawnId,
3015
+ message: serializeError(error).message
3016
+ });
3017
+ }
3018
+ }, { env: frame.env });
3019
+ shard = {
3020
+ key,
3021
+ link,
3022
+ scope: link.managerId,
3023
+ spawnId: frame.spawnId,
3024
+ context,
3025
+ transport,
3026
+ ready: false,
3027
+ exited: false
3028
+ };
3029
+ } catch (error) {
3030
+ this.#report(link, FrameType.Error, {
3031
+ spawnId: frame.spawnId,
3032
+ message: serializeError(error).message
3033
+ });
3034
+ this.#report(link, FrameType.Exit, {
3035
+ spawnId: frame.spawnId,
3036
+ code: null
3037
+ });
3038
+ return;
3039
+ }
3040
+ this.#shards.set(key, shard);
3041
+ this.#byChannel.set(channelKey(shard.scope, context.id), shard);
3042
+ this.#report(link, FrameType.Spawned, {
3043
+ spawnId: frame.spawnId,
3044
+ pid: shard.transport.pid ?? null,
3045
+ threadId: shard.transport.threadId ?? null
3046
+ });
3047
+ this.#reportLoad();
3048
+ this.emit("spawn", context);
3049
+ }
3050
+ #exited(shard, code) {
3051
+ shard.exited = true;
3052
+ this.#shards.delete(shard.key);
3053
+ const key = channelKey(shard.scope, shard.context.id);
3054
+ if (this.#byChannel.get(key) === shard) this.#byChannel.delete(key);
3055
+ const stopped = serializeError(new ShardUnavailableError(shard.context.id, "it stopped"));
3056
+ for (const [nonce, route] of this.#routes) if (route.target === shard) {
3057
+ this.#routes.delete(nonce);
3058
+ this.#toEndpoint(route.origin, shard, {
3059
+ op: Op.Reply,
3060
+ nonce: route.originNonce,
3061
+ error: stopped
3062
+ });
3063
+ } else if ("shard" in route.origin && route.origin.shard === shard) {
3064
+ this.#routes.delete(nonce);
3065
+ this.#toShard(route.target, {
3066
+ op: Op.Abort,
3067
+ nonce
3068
+ });
3069
+ }
3070
+ for (const [requestKey, request] of this.#peerRequests) {
3071
+ if (request.origin !== shard) continue;
3072
+ this.#peerRequests.delete(requestKey);
3073
+ const nonce = Number(requestKey.slice(requestKey.lastIndexOf(":") + 1));
3074
+ this.#abortPeerRequest(request, nonce);
3075
+ }
3076
+ this.#report(shard.link, FrameType.Exit, {
3077
+ spawnId: shard.spawnId,
3078
+ code
3079
+ });
3080
+ this.#reportLoad();
3081
+ this.emit("exit", shard.context, code);
3082
+ }
3083
+ async #fromShard(shard, data) {
3084
+ if (this.#options.localRouting !== false) {
3085
+ let packet = null;
3086
+ try {
3087
+ packet = await this.#codec(shard.context).decode(data, { channelId: shard.context.id });
3088
+ } catch {}
3089
+ if (packet?.op === Op.Signal) {
3090
+ const ready = packet.status === ShardStatus.Ready;
3091
+ if (ready !== shard.ready) {
3092
+ shard.ready = ready;
3093
+ this.#report(shard.link, FrameType.Status, {
3094
+ spawnId: shard.spawnId,
3095
+ ready
3096
+ });
3097
+ }
3098
+ } else if (packet && await this.#route(shard, packet, data)) return;
3099
+ }
3100
+ await shard.link.connection?.sendData(shard.spawnId, data).catch(() => void 0);
3101
+ }
3102
+ async #route(origin, packet, data) {
3103
+ switch (packet.op) {
3104
+ case Op.Message:
3105
+ case Op.Request: {
3106
+ if (typeof packet.to !== "number" || packet.op === Op.Request && packet.system) return false;
3107
+ const local = this.#byChannel.get(channelKey(origin.scope, packet.to));
3108
+ if (local?.ready) {
3109
+ await this.#deliver({ shard: origin }, origin.context.id, local, packet);
3110
+ return true;
3111
+ }
3112
+ const entry = origin.link.directory.get(packet.to);
3113
+ if (!entry || entry.proxy === this.name || !isChannelData(data)) return false;
3114
+ const peer = this.#peer(entry);
3115
+ if (!peer) return false;
3116
+ const header = {
3117
+ scope: origin.scope,
3118
+ origin: origin.context.id,
3119
+ target: packet.to,
3120
+ binary: typeof data !== "string"
3121
+ };
3122
+ if (packet.op === Op.Request) this.#peerRequests.set(`${origin.key}:${packet.nonce}`, {
3123
+ origin,
3124
+ peer,
3125
+ header
3126
+ });
3127
+ await peer.connection.sendPeer(header, data).catch(() => void 0);
3128
+ return true;
3129
+ }
3130
+ case Op.Reply: {
3131
+ const route = this.#routes.get(packet.nonce);
3132
+ if (!route || route.target !== origin) return false;
3133
+ this.#routes.delete(packet.nonce);
3134
+ await this.#toEndpoint(route.origin, origin, {
3135
+ ...packet,
3136
+ nonce: route.originNonce
3137
+ });
3138
+ return true;
3139
+ }
3140
+ case Op.Abort: {
3141
+ for (const [nonce, route] of this.#routes) {
3142
+ if (!("shard" in route.origin) || route.origin.shard !== origin) continue;
3143
+ if (route.originNonce !== packet.nonce) continue;
3144
+ this.#routes.delete(nonce);
3145
+ await this.#toShard(route.target, {
3146
+ op: Op.Abort,
3147
+ nonce
3148
+ });
3149
+ return true;
3150
+ }
3151
+ const request = this.#peerRequests.get(`${origin.key}:${packet.nonce}`);
3152
+ if (!request || !isChannelData(data)) return false;
3153
+ this.#peerRequests.delete(`${origin.key}:${packet.nonce}`);
3154
+ await request.peer.connection.sendPeer({
3155
+ ...request.header,
3156
+ binary: typeof data !== "string"
3157
+ }, data).catch(() => void 0);
3158
+ return true;
3159
+ }
3160
+ default: return false;
3161
+ }
3162
+ }
3163
+ async #deliver(origin, from, target, packet) {
3164
+ if (packet.op === Op.Message) {
3165
+ await this.#toShard(target, {
3166
+ op: Op.Message,
3167
+ body: packet.body,
3168
+ from
3169
+ });
3170
+ return;
3171
+ }
3172
+ if (packet.op !== Op.Request) return;
3173
+ const nonce = --this.#nextNonce;
3174
+ this.#routes.set(nonce, {
3175
+ origin,
3176
+ originNonce: packet.nonce,
3177
+ target
3178
+ });
3179
+ await this.#toShard(target, {
3180
+ op: Op.Request,
3181
+ nonce,
3182
+ body: packet.body,
3183
+ from,
3184
+ timeout: packet.timeout
3185
+ });
3186
+ }
3187
+ async #fromPeer(peer, header, data) {
3188
+ if (header.bounce) {
3189
+ const origin = this.#byChannel.get(channelKey(header.scope, header.origin));
3190
+ if (!origin) return;
3191
+ const packet = await this.#codec(origin.context).decode(data, { channelId: origin.context.id }).catch(() => null);
3192
+ if (packet?.op === Op.Request) this.#peerRequests.delete(`${origin.key}:${packet.nonce}`);
3193
+ await origin.link.connection?.sendData(origin.spawnId, data).catch(() => void 0);
3194
+ return;
3195
+ }
3196
+ const target = this.#byChannel.get(channelKey(header.scope, header.target));
3197
+ let packet = null;
3198
+ if (target) try {
3199
+ packet = await this.#codec(target.context).decode(data, { channelId: header.origin });
3200
+ } catch {
3201
+ packet = null;
3202
+ }
3203
+ if (!target || !packet) {
3204
+ await peer.connection.sendPeer({
3205
+ ...header,
3206
+ bounce: true
3207
+ }, data).catch(() => void 0);
3208
+ return;
3209
+ }
3210
+ switch (packet.op) {
3211
+ case Op.Message:
3212
+ case Op.Request:
3213
+ if (!target.ready) {
3214
+ await peer.connection.sendPeer({
3215
+ ...header,
3216
+ bounce: true
3217
+ }, data).catch(() => void 0);
3218
+ return;
3219
+ }
3220
+ await this.#deliver({
3221
+ peer,
3222
+ channel: header.origin
3223
+ }, header.origin, target, packet);
3224
+ break;
3225
+ case Op.Abort:
3226
+ for (const [nonce, route] of this.#routes) {
3227
+ if (!("peer" in route.origin) || route.origin.peer !== peer) continue;
3228
+ if (route.origin.channel !== header.origin || route.originNonce !== packet.nonce) continue;
3229
+ this.#routes.delete(nonce);
3230
+ await this.#toShard(route.target, {
3231
+ op: Op.Abort,
3232
+ nonce
3233
+ });
3234
+ break;
3235
+ }
3236
+ break;
3237
+ case Op.Reply:
3238
+ this.#peerRequests.delete(`${target.key}:${packet.nonce}`);
3239
+ await this.#toShard(target, packet);
3240
+ }
3241
+ }
3242
+ async #abortPeerRequest(request, nonce) {
3243
+ const data = await this.#codec(request.origin.context).encode({
3244
+ op: Op.Abort,
3245
+ nonce
3246
+ }, { channelId: request.origin.context.id });
3247
+ if (!isChannelData(data)) return;
3248
+ await request.peer.connection.sendPeer({
3249
+ ...request.header,
3250
+ binary: typeof data !== "string"
3251
+ }, data).catch(() => void 0);
3252
+ }
3253
+ async #toEndpoint(endpoint, from, packet) {
3254
+ if ("shard" in endpoint) {
3255
+ await this.#toShard(endpoint.shard, packet);
3256
+ return;
3257
+ }
3258
+ const data = await this.#codec(from.context).encode(packet, { channelId: from.context.id });
3259
+ if (!isChannelData(data)) return;
3260
+ const header = {
3261
+ scope: from.scope,
3262
+ origin: from.context.id,
3263
+ target: endpoint.channel,
3264
+ binary: typeof data !== "string"
3265
+ };
3266
+ await endpoint.peer.connection.sendPeer(header, data).catch(() => void 0);
3267
+ }
3268
+ async #toShard(shard, packet) {
3269
+ if (shard.exited) return;
3270
+ const data = await this.#codec(shard.context).encode(packet, { channelId: shard.context.id });
3271
+ await shard.transport.send(data).catch(() => void 0);
3272
+ }
3273
+ #codec(context) {
3274
+ const key = JSON.stringify([context.messageHandler, context.transformers]);
3275
+ let codec = this.#codecs.get(key);
3276
+ if (!codec) {
3277
+ codec = new PacketCodec(resolveMessageHandler(context.messageHandler), context.transformers.map(resolveMessageTransformer));
3278
+ this.#codecs.set(key, codec);
3279
+ }
3280
+ return codec;
3281
+ }
3282
+ async #listenPeers() {
3283
+ const options = this.#options.peer;
3284
+ if (!options || this.#peerServer) return;
3285
+ const onSocket = (socket) => this.#acceptPeer(socket);
3286
+ const server = options.tls ? createServer$1(options.tls, onSocket) : createServer(onSocket);
3287
+ await new Promise((resolve, reject) => {
3288
+ server.once("error", reject);
3289
+ server.listen(options.port, options.host, () => {
3290
+ server.off("error", reject);
3291
+ resolve();
3292
+ });
3293
+ });
3294
+ const address = server.address();
3295
+ this.#peerServer = server;
3296
+ this.#peerAddress = {
3297
+ host: options.advertise ?? options.host ?? hostname(),
3298
+ port: typeof address === "object" && address ? address.port : options.port
3299
+ };
3300
+ }
3301
+ #acceptPeer(socket) {
3302
+ const connection = new Connection(socket, this.#heartbeat());
3303
+ let peer = null;
3304
+ connection.on("frame", (type, payload) => {
3305
+ if (peer) {
3306
+ if (type === FrameType.PeerPacket) {
3307
+ const { header, data } = readPeer(payload);
3308
+ this.#fromPeer(peer, header, data);
3309
+ }
3310
+ return;
3311
+ }
3312
+ const hello = type === FrameType.PeerHello ? JSON.parse(payload.toString()) : null;
3313
+ if (!hello || !tokensMatch(this.#peerToken(), hello.token)) {
3314
+ connection.close();
3315
+ return;
3316
+ }
3317
+ peer = {
3318
+ name: hello.name,
3319
+ connection
3320
+ };
3321
+ this.#inbound.add(peer);
3322
+ connection.send(FrameType.PeerWelcome).catch(() => void 0);
3323
+ this.emit("peerConnect", peer.name);
3324
+ });
3325
+ connection.on("close", () => {
3326
+ if (!peer) return;
3327
+ this.#inbound.delete(peer);
3328
+ this.#peerLost(peer);
3329
+ });
3330
+ }
3331
+ #peer(entry) {
3332
+ const key = `${entry.host}:${entry.port}`;
3333
+ const peer = this.#peers.get(key);
3334
+ if (peer && !peer.connection.closed) return peer;
3335
+ for (const inbound of this.#inbound) if (inbound.name === entry.proxy && !inbound.connection.closed) return inbound;
3336
+ if (this.#connecting.has(key) || this.#destroyed) return null;
3337
+ this.#connecting.add(key);
3338
+ const tls = this.#options.peer?.connectTls;
3339
+ const socket = tls ? connect$1({
3340
+ ...tls === true ? {} : tls,
3341
+ host: entry.host,
3342
+ port: entry.port
3343
+ }) : connect({
3344
+ host: entry.host,
3345
+ port: entry.port
3346
+ });
3347
+ socket.once("error", () => this.#connecting.delete(key));
3348
+ socket.once(tls ? "secureConnect" : "connect", () => {
3349
+ socket.removeAllListeners("error");
3350
+ const connection = new Connection(socket, this.#heartbeat());
3351
+ const opened = {
3352
+ name: entry.proxy,
3353
+ connection
3354
+ };
3355
+ connection.on("frame", (type, payload) => {
3356
+ if (type === FrameType.PeerWelcome) {
3357
+ this.#connecting.delete(key);
3358
+ this.#peers.set(key, opened);
3359
+ this.emit("peerConnect", opened.name);
3360
+ } else if (type === FrameType.PeerPacket) {
3361
+ const { header, data } = readPeer(payload);
3362
+ this.#fromPeer(opened, header, data);
3363
+ }
3364
+ });
3365
+ connection.on("close", () => {
3366
+ this.#connecting.delete(key);
3367
+ if (this.#peers.get(key) === opened) this.#peers.delete(key);
3368
+ this.#peerLost(opened);
3369
+ });
3370
+ connection.sendJson(FrameType.PeerHello, {
3371
+ token: this.#peerToken(),
3372
+ name: this.name
3373
+ }).catch(() => void 0);
3374
+ });
3375
+ return null;
3376
+ }
3377
+ #peerLost(peer) {
3378
+ for (const [key, request] of this.#peerRequests) {
3379
+ if (request.peer !== peer) continue;
3380
+ this.#peerRequests.delete(key);
3381
+ const nonce = Number(key.slice(key.lastIndexOf(":") + 1));
3382
+ const error = serializeError(new ShardUnavailableError(request.header.target, "its proxy is unreachable"));
3383
+ this.#toShard(request.origin, {
3384
+ op: Op.Reply,
3385
+ nonce,
3386
+ error
3387
+ });
3388
+ }
3389
+ for (const [nonce, route] of this.#routes) {
3390
+ if (!("peer" in route.origin) || route.origin.peer !== peer) continue;
3391
+ this.#routes.delete(nonce);
3392
+ this.#toShard(route.target, {
3393
+ op: Op.Abort,
3394
+ nonce
3395
+ });
3396
+ }
3397
+ this.emit("peerDisconnect", peer.name);
3398
+ }
3399
+ #available() {
3400
+ return Math.max(this.capacity - this.#shards.size, 0);
3401
+ }
3402
+ #reportLoad() {
3403
+ for (const link of this.#links) this.#report(link, FrameType.Load, { available: this.#available() });
3404
+ }
3405
+ async #report(link, type, body) {
3406
+ await link.connection?.sendJson(type, body).catch(() => void 0);
3407
+ }
3408
+ #peerToken() {
3409
+ return this.#options.peer?.token ?? this.#options.token;
3410
+ }
3411
+ #heartbeat() {
3412
+ return {
3413
+ interval: this.#options.heartbeat?.interval ?? 15e3,
3414
+ timeout: this.#options.heartbeat?.timeout ?? 45e3
3415
+ };
3416
+ }
3417
+ };
3418
+ function createLink(addresses, index) {
3419
+ return {
3420
+ index,
3421
+ addresses,
3422
+ address: null,
3423
+ connection: null,
3424
+ managerId: null,
3425
+ directory: /* @__PURE__ */ new Map()
3426
+ };
3427
+ }
3428
+ function shardKey(link, spawnId) {
3429
+ return `${link.index}:${spawnId}`;
3430
+ }
3431
+ function channelKey(scope, channel) {
3432
+ return `${scope}:${channel}`;
3433
+ }
3434
+ function isChannelData(data) {
3435
+ return typeof data === "string" || data instanceof Uint8Array;
3436
+ }
3437
+ function parseAddress(address) {
3438
+ if (typeof address !== "string") return address;
3439
+ const separator = address.lastIndexOf(":");
3440
+ const port = Number(address.slice(separator + 1));
3441
+ if (separator === -1 || !Number.isInteger(port)) throw new RangeError(`"${address}" is not a host:port address`);
3442
+ return {
3443
+ host: address.slice(0, separator).replace(/^\[|\]$/g, ""),
3444
+ port
3445
+ };
3446
+ }
3447
+
3448
+ //#endregion
3449
+ //#region src/util/commands.ts
3450
+ /**
3451
+ * Builds a request handler answering {@link CommandRequest}s: the sharder stays raw data (as the RFC settled), and
3452
+ * this is the optional, typed layer on top.
3453
+ *
3454
+ * @param commands The commands, by name.
3455
+ * @example
3456
+ * ```ts
3457
+ * const commands = {
3458
+ * guildCount: () => guilds.size,
3459
+ * guild: (id: string) => guilds.get(id) ?? null,
3460
+ * } satisfies Commands;
3461
+ *
3462
+ * shard.setRequestHandler(createCommandHandler(commands));
3463
+ * const counts = await manager.broadcastRequest<CommandReply<typeof commands, "guildCount">>(
3464
+ * command<typeof commands>("guildCount"),
3465
+ * );
3466
+ * ```
3467
+ */
3468
+ function createCommandHandler(commands) {
3469
+ return (body, context) => {
3470
+ const request = body;
3471
+ const run = typeof request?.command === "string" ? commands[request.command] : void 0;
3472
+ if (!run || !Object.hasOwn(commands, request.command)) throw new RangeError(`Unknown command ${String(request?.command)}`);
3473
+ return run(request.data, context);
3474
+ };
3475
+ }
3476
+ /**
3477
+ * Builds a {@link CommandRequest}.
3478
+ *
3479
+ * @param name The name of the command.
3480
+ * @param data Its argument.
3481
+ */
3482
+ function command(name, ...data) {
3483
+ return {
3484
+ command: name,
3485
+ data: data[0]
3486
+ };
3487
+ }
3488
+
3489
+ //#endregion
3490
+ export { BrotliTransformer, ClusterStrategy, ForkStrategy, GzipTransformer, JsonMessageHandler, NetworkStrategy, ProcessStrategy, RawMessageHandler, Result, ShardChannel, ShardClient, ShardContextVariable, ShardManager, ShardManagerProxy, ShardPing, ShardRequestError, ShardRequestTimeoutError, ShardSpawnError, ShardStatus, ShardUnavailableError, V8MessageHandler, WorkerStrategy, command, createCommandHandler, encodeContext, fetchGatewayInformation, fetchRecommendedShardCount, registerMessageHandler, registerMessageTransformer, registerStrategy, resolveMessageHandler, resolveMessageTransformer, resolveRecommendedShardCount, resolveStrategy, shardIdForGuild };
3491
+ //# sourceMappingURL=index.js.map