lunibee 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +1 -1
  2. package/dist/builders/commands.d.ts +1 -1
  3. package/dist/builders/components.d.ts +9 -11
  4. package/dist/builders/embed.d.ts +1 -1
  5. package/dist/builders/index.d.ts +1 -1
  6. package/dist/builders/index.js +87 -1298
  7. package/dist/builders/index.js.map +3 -11
  8. package/dist/chunks/index-0wn81eng.js +2492 -0
  9. package/dist/chunks/index-0wn81eng.js.map +17 -0
  10. package/dist/chunks/index-2en489j6.js +40 -0
  11. package/dist/chunks/index-2en489j6.js.map +10 -0
  12. package/dist/chunks/index-3hyps48a.js +203 -0
  13. package/dist/chunks/index-3hyps48a.js.map +11 -0
  14. package/dist/chunks/index-57c5n3ms.js +2274 -0
  15. package/dist/chunks/index-57c5n3ms.js.map +19 -0
  16. package/dist/chunks/index-5yx92e36.js +1170 -0
  17. package/dist/chunks/index-5yx92e36.js.map +14 -0
  18. package/dist/chunks/index-690bp093.js +95 -0
  19. package/dist/chunks/index-690bp093.js.map +10 -0
  20. package/dist/chunks/index-7fp2hvmg.js +2155 -0
  21. package/dist/chunks/index-7fp2hvmg.js.map +24 -0
  22. package/dist/chunks/index-cnmsajvk.js +1119 -0
  23. package/dist/chunks/index-cnmsajvk.js.map +12 -0
  24. package/dist/chunks/index-fdvgk2sw.js +1310 -0
  25. package/dist/chunks/index-fdvgk2sw.js.map +21 -0
  26. package/dist/chunks/index-gyvps8b9.js +214 -0
  27. package/dist/chunks/index-gyvps8b9.js.map +12 -0
  28. package/dist/chunks/index-jzg4bh99.js +1533 -0
  29. package/dist/chunks/index-jzg4bh99.js.map +21 -0
  30. package/dist/chunks/index-mgw2yvrm.js +3547 -0
  31. package/dist/chunks/index-mgw2yvrm.js.map +25 -0
  32. package/dist/chunks/index-yh76b55h.js +3449 -0
  33. package/dist/chunks/index-yh76b55h.js.map +29 -0
  34. package/dist/collection/index.js +9 -1118
  35. package/dist/collection/index.js.map +3 -6
  36. package/dist/core/api.d.ts +3 -3
  37. package/dist/core/channel-create.d.ts +2 -2
  38. package/dist/core/commands.d.ts +3 -3
  39. package/dist/core/events.d.ts +4 -4
  40. package/dist/core/handles.d.ts +4 -4
  41. package/dist/core/index.d.ts +25 -20
  42. package/dist/core/index.js +68 -10130
  43. package/dist/core/index.js.map +3 -65
  44. package/dist/core/permissions.d.ts +1 -1
  45. package/dist/core/redis-cache.d.ts +1 -1
  46. package/dist/formatters/index.js +41 -110
  47. package/dist/formatters/index.js.map +3 -4
  48. package/dist/handlers/index.js +5 -37
  49. package/dist/handlers/index.js.map +3 -4
  50. package/dist/index.d.ts +21 -21
  51. package/dist/index.js +641 -16325
  52. package/dist/index.js.map +3 -92
  53. package/dist/managers/advanced.d.ts +3 -3
  54. package/dist/managers/application.d.ts +2 -2
  55. package/dist/managers/base.d.ts +1 -1
  56. package/dist/managers/emoji.d.ts +2 -2
  57. package/dist/managers/guild-resources.d.ts +6 -6
  58. package/dist/managers/guild.d.ts +3 -3
  59. package/dist/managers/index.d.ts +4 -8
  60. package/dist/managers/index.js +51 -4060
  61. package/dist/managers/index.js.map +3 -29
  62. package/dist/managers/member.d.ts +2 -2
  63. package/dist/managers/message.d.ts +14 -6
  64. package/dist/managers/role.d.ts +2 -2
  65. package/dist/managers/thread.d.ts +3 -3
  66. package/dist/managers/user.d.ts +7 -4
  67. package/dist/rest/index.d.ts +10 -35
  68. package/dist/rest/index.js +57 -2184
  69. package/dist/rest/index.js.map +3 -18
  70. package/dist/rest/limiter.d.ts +9 -1
  71. package/dist/rest/webhook.d.ts +2 -8
  72. package/dist/sharding/bus.d.ts +2 -2
  73. package/dist/sharding/cluster.d.ts +45 -5
  74. package/dist/sharding/context.d.ts +45 -3
  75. package/dist/sharding/gateway-bot.d.ts +2 -1
  76. package/dist/sharding/health.d.ts +1 -1
  77. package/dist/sharding/identify.d.ts +12 -0
  78. package/dist/sharding/index.d.ts +44 -11
  79. package/dist/sharding/index.js +60 -4620
  80. package/dist/sharding/index.js.map +3 -33
  81. package/dist/sharding/supervisor.d.ts +4 -2
  82. package/dist/structures/audit-log.d.ts +1 -1
  83. package/dist/structures/base.d.ts +6 -6
  84. package/dist/structures/channels.d.ts +1 -1
  85. package/dist/structures/embed.d.ts +1 -1
  86. package/dist/structures/index.d.ts +14 -14
  87. package/dist/structures/index.js +109 -2382
  88. package/dist/structures/index.js.map +3 -16
  89. package/dist/structures/interactions.d.ts +13 -9
  90. package/dist/structures/options.d.ts +1 -1
  91. package/dist/structures/permissions.d.ts +2 -2
  92. package/dist/structures/resources.d.ts +10 -10
  93. package/dist/structures/sticker.d.ts +1 -1
  94. package/dist/types/gateway.d.ts +2 -79
  95. package/dist/types/index.d.ts +0 -6
  96. package/dist/types/index.js +41 -272
  97. package/dist/types/index.js.map +3 -6
  98. package/dist/utils/index.js +23 -209
  99. package/dist/utils/index.js.map +3 -5
  100. package/dist/voice/connection.d.ts +200 -0
  101. package/dist/voice/crypto.d.ts +50 -0
  102. package/dist/voice/dave.d.ts +85 -0
  103. package/dist/voice/effects.d.ts +31 -0
  104. package/dist/voice/emitter.d.ts +14 -0
  105. package/dist/voice/gateway.d.ts +59 -0
  106. package/dist/voice/index.d.ts +25 -228
  107. package/dist/voice/index.js +111 -442
  108. package/dist/voice/index.js.map +3 -4
  109. package/dist/voice/libopus.d.ts +88 -0
  110. package/dist/voice/networking.d.ts +80 -0
  111. package/dist/voice/ogg.d.ts +24 -0
  112. package/dist/voice/opus.d.ts +18 -0
  113. package/dist/voice/player.d.ts +79 -0
  114. package/dist/voice/protocol.d.ts +76 -0
  115. package/dist/voice/receiver.d.ts +59 -0
  116. package/dist/voice/resource.d.ts +102 -0
  117. package/dist/voice/udp.d.ts +40 -0
  118. package/dist/ws/close-codes.d.ts +1 -1
  119. package/dist/ws/index.d.ts +25 -4
  120. package/dist/ws/index.js +47 -1336
  121. package/dist/ws/index.js.map +3 -17
  122. package/dist/ws/protocol.d.ts +1 -1
  123. package/dist/ws/send-budget.d.ts +0 -2
  124. package/dist/ws/state.d.ts +0 -10
  125. package/package.json +19 -16
@@ -0,0 +1,3547 @@
1
+ // @bun
2
+ import {
3
+ Gateway2
4
+ } from "./index-fdvgk2sw.js";
5
+ // packages/sharding/package.json
6
+ var package_default = {
7
+ name: "@lunibee/sharding",
8
+ version: "0.4.0",
9
+ type: "module",
10
+ module: "./src/index.ts",
11
+ types: "./src/index.ts",
12
+ exports: {
13
+ ".": "./src/index.ts"
14
+ },
15
+ dependencies: {
16
+ "@lunibee/ws": "workspace:*"
17
+ }
18
+ };
19
+
20
+ // packages/sharding/src/gateway-bot.ts
21
+ async function fetchGatewayBot2(token) {
22
+ const response = await fetch("https://discord.com/api/v10/gateway/bot", {
23
+ headers: {
24
+ Authorization: `Bot ${token}`,
25
+ "User-Agent": `Lunibee/${package_default.version}`
26
+ }
27
+ });
28
+ if (!response.ok)
29
+ throw new Error(`Gateway discovery failed with status ${response.status}`);
30
+ const data = await response.json();
31
+ if (typeof data.shards !== "number" || !Number.isInteger(data.shards) || data.shards < 1)
32
+ throw new Error("Gateway discovery returned an invalid shard count.");
33
+ const limit = data.session_start_limit;
34
+ const info = { shards: data.shards };
35
+ if (limit && typeof limit.total === "number" && typeof limit.remaining === "number" && typeof limit.reset_after === "number" && typeof limit.max_concurrency === "number")
36
+ info.sessionStartLimit = {
37
+ total: limit.total,
38
+ remaining: limit.remaining,
39
+ resetAfter: limit.reset_after,
40
+ maxConcurrency: Math.max(1, limit.max_concurrency)
41
+ };
42
+ return info;
43
+ }
44
+ var MAX_GUILDS_PER_SHARD2 = 2500;
45
+ function assertGuildsPerShard(value) {
46
+ if (!Number.isInteger(value) || value < 1 || value > MAX_GUILDS_PER_SHARD2)
47
+ throw new RangeError(`guildsPerShard must be an integer from 1 to ${MAX_GUILDS_PER_SHARD2}.`);
48
+ }
49
+ function shardsForGuilds2(guilds, guildsPerShard) {
50
+ assertGuildsPerShard(guildsPerShard);
51
+ if (!Number.isInteger(guilds) || guilds < 0)
52
+ throw new RangeError("guildCount must be a non-negative integer.");
53
+ return Math.max(1, Math.ceil(guilds / guildsPerShard));
54
+ }
55
+ async function fetchGuildCount2(token) {
56
+ const response = await fetch("https://discord.com/api/v10/applications/@me", {
57
+ headers: {
58
+ Authorization: `Bot ${token}`,
59
+ "User-Agent": `Lunibee/${package_default.version}`
60
+ }
61
+ });
62
+ if (!response.ok)
63
+ throw new Error(`Guild count discovery failed with status ${response.status}`);
64
+ const data = await response.json();
65
+ const count = data.approximate_guild_count;
66
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 0)
67
+ throw new Error("Discord did not report a guild count.");
68
+ return count;
69
+ }
70
+ async function resolveShardCount(token, info, options) {
71
+ if (options.guildsPerShard === undefined)
72
+ return info.shards;
73
+ return Math.max(info.shards, shardsForGuilds2(options.guildCount ?? await fetchGuildCount2(token), options.guildsPerShard));
74
+ }
75
+
76
+ // packages/sharding/src/context.ts
77
+ var WORKER_MARKER = "LUNIBEE_CLUSTER_WORKER";
78
+ function isClusterWorker2(env = process.env) {
79
+ return env[WORKER_MARKER] === "1";
80
+ }
81
+ function validateShardIds2(shardIds, total) {
82
+ if (!Number.isInteger(total) || total < 1)
83
+ throw new RangeError("Shard count must be a positive integer.");
84
+ const seen = new Set;
85
+ for (const id of shardIds) {
86
+ if (!Number.isInteger(id) || id < 0 || id >= total)
87
+ throw new RangeError(`Shard ID ${String(id)} is outside 0-${total - 1}.`);
88
+ if (seen.has(id))
89
+ throw new RangeError(`Shard ID ${id} is listed twice.`);
90
+ seen.add(id);
91
+ }
92
+ }
93
+ function allocateShards2(shardIds, clusterCount) {
94
+ if (!Number.isInteger(clusterCount) || clusterCount < 1)
95
+ throw new RangeError("clusterCount must be a positive integer.");
96
+ const base = Math.floor(shardIds.length / clusterCount);
97
+ const extra = shardIds.length % clusterCount;
98
+ const chunks = [];
99
+ let offset = 0;
100
+ for (let cluster = 0;cluster < clusterCount; cluster++) {
101
+ const size = base + (cluster < extra ? 1 : 0);
102
+ chunks.push(shardIds.slice(offset, offset + size));
103
+ offset += size;
104
+ }
105
+ return chunks;
106
+ }
107
+ function positiveInteger(name, value) {
108
+ if (!Number.isInteger(value) || value < 1)
109
+ throw new RangeError(`${name} must be a positive integer, got ${String(value)}.`);
110
+ }
111
+ function planClusters2(options) {
112
+ const { shardIds, clusterCount, shardsPerCluster } = options;
113
+ if (clusterCount !== undefined && shardsPerCluster !== undefined)
114
+ throw new RangeError("Set either clusterCount or shardsPerCluster, not both: the cluster count follows from shardsPerCluster.");
115
+ if (shardIds.length === 0)
116
+ throw new RangeError("There are no shards to allocate.");
117
+ let requested;
118
+ let source;
119
+ let clusters;
120
+ if (shardsPerCluster !== undefined) {
121
+ positiveInteger("shardsPerCluster", shardsPerCluster);
122
+ source = "shardsPerCluster";
123
+ requested = Math.ceil(shardIds.length / shardsPerCluster);
124
+ clusters = [];
125
+ for (let i = 0;i < shardIds.length; i += shardsPerCluster)
126
+ clusters.push(shardIds.slice(i, i + shardsPerCluster));
127
+ } else {
128
+ requested = clusterCount ?? options.defaultClusterCount;
129
+ positiveInteger(clusterCount === undefined ? "defaultClusterCount" : "clusterCount", requested);
130
+ source = clusterCount === undefined ? "default" : "clusterCount";
131
+ clusters = allocateShards2(shardIds, Math.min(requested, shardIds.length));
132
+ }
133
+ assertCompleteAllocation2(clusters, shardIds);
134
+ return { clusters, source, requested };
135
+ }
136
+ function assertCompleteAllocation2(chunks, expected) {
137
+ const owners = new Map;
138
+ const duplicate = [];
139
+ chunks.forEach((chunk, cluster) => {
140
+ for (const id of chunk) {
141
+ if (owners.has(id))
142
+ duplicate.push(id);
143
+ owners.set(id, cluster);
144
+ }
145
+ });
146
+ const want = new Set(expected);
147
+ const missing = expected.filter((id) => !owners.has(id));
148
+ const extra = [...owners.keys()].filter((id) => !want.has(id));
149
+ if (duplicate.length || missing.length || extra.length)
150
+ throw new Error(`Shard allocation is not exact: duplicate [${duplicate}], missing [${missing}], unexpected [${extra}].`);
151
+ }
152
+ function parseInteger(name, value) {
153
+ if (value === undefined || !/^\d+$/.test(value))
154
+ throw new RangeError(`${name} must be a non-negative integer.`);
155
+ return Number(value);
156
+ }
157
+ function readClusterContext2(env = process.env) {
158
+ if (!isClusterWorker2(env))
159
+ return null;
160
+ const shardCount = parseInteger("SHARD_COUNT", env.SHARD_COUNT);
161
+ const shardIds = (env.SHARD_LIST ?? "").split(",").filter((part) => part !== "").map((part) => parseInteger("SHARD_LIST", part));
162
+ validateShardIds2(shardIds, shardCount);
163
+ if (shardIds.length === 0)
164
+ throw new RangeError("SHARD_LIST names no shards for this cluster.");
165
+ const clusterId = parseInteger("CLUSTER_ID", env.CLUSTER_ID);
166
+ const clusterCount = parseInteger("CLUSTER_COUNT", env.CLUSTER_COUNT);
167
+ if (clusterCount < 1 || clusterId >= clusterCount)
168
+ throw new RangeError(`CLUSTER_ID ${clusterId} is outside the ${clusterCount} cluster(s).`);
169
+ return {
170
+ clusterId,
171
+ clusterCount,
172
+ shardCount,
173
+ shardIds,
174
+ coordinatedIdentify: env.LUNIBEE_IDENTIFY === "coordinated",
175
+ startup: env.LUNIBEE_STARTUP === "sequential" ? "sequential" : "safe",
176
+ ...env.LUNIBEE_READY_TIMEOUT === undefined ? {} : {
177
+ readyTimeout: Math.max(1, parseInteger("LUNIBEE_READY_TIMEOUT", env.LUNIBEE_READY_TIMEOUT))
178
+ }
179
+ };
180
+ }
181
+
182
+ // packages/sharding/src/protocol.ts
183
+ var CLUSTER_FRAME = "__lunibeeCluster";
184
+ var PARENT_ID = -1;
185
+ var REQUEST_HEALTH = "__lunibee:health";
186
+ var REQUEST_ASSIGN = "__lunibee:assign";
187
+ var isRecord = (value) => typeof value === "object" && value !== null;
188
+ var isShardId = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
189
+ function parseChildFrame(value) {
190
+ if (!isRecord(value))
191
+ return null;
192
+ switch (value.kind) {
193
+ case "ready":
194
+ return Array.isArray(value.shardIds) && value.shardIds.every(isShardId) ? { kind: "ready", shardIds: value.shardIds } : null;
195
+ case "identify":
196
+ return typeof value.id === "number" && isShardId(value.shardId) ? { kind: "identify", id: value.id, shardId: value.shardId } : null;
197
+ case "identify-done":
198
+ return isShardId(value.shardId) ? { kind: "identify-done", shardId: value.shardId } : null;
199
+ case "reply":
200
+ return typeof value.id === "string" && (value.error === undefined || typeof value.error === "string") ? {
201
+ kind: "reply",
202
+ id: value.id,
203
+ ...value.error !== undefined ? { error: value.error } : { result: value.result }
204
+ } : null;
205
+ case "health":
206
+ return isRecord(value.report) && Array.isArray(value.report.shards) && typeof value.report.reportedAt === "number" ? { kind: "health", report: value.report } : null;
207
+ default:
208
+ return null;
209
+ }
210
+ }
211
+ function parseParentFrame(value) {
212
+ if (!isRecord(value))
213
+ return null;
214
+ switch (value.kind) {
215
+ case "identify-grant":
216
+ return typeof value.id === "number" ? { kind: "identify-grant", id: value.id } : null;
217
+ case "identify-deny":
218
+ return typeof value.id === "number" && typeof value.error === "string" ? { kind: "identify-deny", id: value.id, error: value.error } : null;
219
+ case "request":
220
+ return typeof value.id === "string" && typeof value.type === "string" ? {
221
+ kind: "request",
222
+ id: value.id,
223
+ type: value.type,
224
+ data: value.data
225
+ } : null;
226
+ default:
227
+ return null;
228
+ }
229
+ }
230
+
231
+ // packages/sharding/src/thread.ts
232
+ import { EventEmitter } from "events";
233
+ import {
234
+ isMainThread,
235
+ parentPort,
236
+ Worker,
237
+ workerData
238
+ } from "worker_threads";
239
+ var THREAD_MARKER = "__lunibeeClusterThread";
240
+
241
+ class ThreadChild extends EventEmitter {
242
+ pid;
243
+ exitCode = null;
244
+ signalCode = null;
245
+ connected = true;
246
+ #worker;
247
+ #signal;
248
+ constructor(worker) {
249
+ super();
250
+ this.#worker = worker;
251
+ this.pid = worker.threadId;
252
+ worker.on("message", (frame) => this.emit("message", frame));
253
+ worker.on("error", (error) => this.emit("error", error));
254
+ worker.once("exit", (code) => {
255
+ this.connected = false;
256
+ if (this.#signal) {
257
+ this.signalCode = this.#signal;
258
+ this.emit("exit", null, this.#signal);
259
+ } else {
260
+ this.exitCode = code;
261
+ this.emit("exit", code, null);
262
+ }
263
+ });
264
+ }
265
+ send(frame) {
266
+ if (!this.connected)
267
+ return false;
268
+ try {
269
+ this.#worker.postMessage(frame);
270
+ return true;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
275
+ kill(signal = "SIGTERM") {
276
+ if (!this.connected)
277
+ return false;
278
+ this.#signal ??= signal;
279
+ this.#worker.terminate();
280
+ return true;
281
+ }
282
+ }
283
+ function threadFork2(options = {}) {
284
+ return (script, env) => {
285
+ const defined = {};
286
+ for (const [key, value] of Object.entries(env))
287
+ if (value !== undefined)
288
+ defined[key] = value;
289
+ const worker = new Worker(script, {
290
+ env: defined,
291
+ workerData: { [THREAD_MARKER]: true },
292
+ ...options.resourceLimits ? { resourceLimits: options.resourceLimits } : {}
293
+ });
294
+ return new ThreadChild(worker);
295
+ };
296
+ }
297
+ var endpoint;
298
+ function threadEndpoint2() {
299
+ const port = parentPort;
300
+ if (isMainThread || !port || !workerData?.[THREAD_MARKER])
301
+ return;
302
+ endpoint ??= portEndpoint(port);
303
+ return endpoint;
304
+ }
305
+ function portEndpoint(port) {
306
+ const wrapped = {
307
+ send: (message) => {
308
+ port.postMessage(message);
309
+ return true;
310
+ },
311
+ on: (event, listener) => {
312
+ if (event === "message")
313
+ port.on("message", listener);
314
+ return wrapped;
315
+ },
316
+ off: (event, listener) => {
317
+ if (event === "message")
318
+ port.off("message", listener);
319
+ return wrapped;
320
+ }
321
+ };
322
+ return wrapped;
323
+ }
324
+
325
+ // packages/sharding/src/worker.ts
326
+ class ClusterWorker2 {
327
+ static #current;
328
+ static current() {
329
+ if (!ClusterWorker2.#current) {
330
+ const context = readClusterContext2();
331
+ const endpoint = typeof process.send === "function" ? process : threadEndpoint2();
332
+ if (context && endpoint)
333
+ ClusterWorker2.#current = new ClusterWorker2(context, endpoint);
334
+ }
335
+ return ClusterWorker2.#current;
336
+ }
337
+ static reset() {
338
+ ClusterWorker2.#current?.close();
339
+ ClusterWorker2.#current = undefined;
340
+ }
341
+ context;
342
+ #endpoint;
343
+ #handlers = new Map;
344
+ #waiting = new Map;
345
+ #onMessage;
346
+ #onDisconnect;
347
+ #counter = 0;
348
+ #closed = false;
349
+ constructor(context, endpoint = process, options = {}) {
350
+ if (typeof endpoint.send !== "function")
351
+ throw new Error("ClusterWorker needs an IPC channel: run it in a process forked by ClusterManager.");
352
+ this.context = context;
353
+ this.#endpoint = endpoint;
354
+ this.#onMessage = (frame) => this.#handle(frame);
355
+ const exitOnLoss = options.exitOnParentLoss ?? endpoint === process;
356
+ const exit = options.exit ?? ((code) => process.exit(code));
357
+ this.#onDisconnect = () => {
358
+ this.#failAll("The parent went away.");
359
+ if (exitOnLoss && !this.#closed)
360
+ exit(1);
361
+ };
362
+ endpoint.on("message", this.#onMessage);
363
+ endpoint.on("disconnect", this.#onDisconnect);
364
+ }
365
+ get identifyGate() {
366
+ return this.context.coordinatedIdentify ? this.#gate : undefined;
367
+ }
368
+ #gate = {
369
+ acquire: (shardId) => {
370
+ if (this.#closed)
371
+ return Promise.reject(new Error("The cluster worker is closed."));
372
+ const id = ++this.#counter;
373
+ return new Promise((resolve, reject) => {
374
+ this.#waiting.set(id, { resolve, reject });
375
+ const error = this.#send({ kind: "identify", id, shardId });
376
+ if (error) {
377
+ this.#waiting.delete(id);
378
+ reject(error);
379
+ }
380
+ });
381
+ },
382
+ release: (shardId) => this.#send({ kind: "identify-done", shardId })
383
+ };
384
+ ready(shardIds = this.context.shardIds) {
385
+ this.#send({ kind: "ready", shardIds: [...shardIds] });
386
+ }
387
+ reportHealth(report) {
388
+ this.#send({ kind: "health", report });
389
+ }
390
+ onRequest(type, handler) {
391
+ if (!type.trim() || typeof handler !== "function")
392
+ throw new TypeError("A request type and handler are required.");
393
+ this.#handlers.set(type, handler);
394
+ return this;
395
+ }
396
+ close() {
397
+ this.#closed = true;
398
+ this.#endpoint.off("message", this.#onMessage);
399
+ this.#endpoint.off("disconnect", this.#onDisconnect);
400
+ this.#failAll("The cluster worker closed.");
401
+ }
402
+ #send(body) {
403
+ try {
404
+ this.#endpoint.send({ [CLUSTER_FRAME]: body });
405
+ return;
406
+ } catch (thrown) {
407
+ const error = thrown instanceof Error ? thrown : new Error(String(thrown));
408
+ const endpoint = this.#endpoint;
409
+ if (endpoint.connected === false || error.code === "ERR_IPC_CHANNEL_CLOSED")
410
+ this.#failAll(error.message);
411
+ return error;
412
+ }
413
+ }
414
+ #failAll(reason) {
415
+ for (const waiter of this.#waiting.values())
416
+ waiter.reject(new Error(reason));
417
+ this.#waiting.clear();
418
+ }
419
+ #handle(frame) {
420
+ if (!frame || typeof frame !== "object" || !(CLUSTER_FRAME in frame))
421
+ return;
422
+ const parsed = parseParentFrame(frame[CLUSTER_FRAME]);
423
+ if (!parsed)
424
+ return;
425
+ if (parsed.kind === "identify-grant" || parsed.kind === "identify-deny") {
426
+ const waiter = this.#waiting.get(parsed.id);
427
+ if (!waiter)
428
+ return;
429
+ this.#waiting.delete(parsed.id);
430
+ if (parsed.kind === "identify-grant")
431
+ waiter.resolve();
432
+ else
433
+ waiter.reject(new Error(parsed.error));
434
+ return;
435
+ }
436
+ const handler = this.#handlers.get(parsed.type);
437
+ const id = parsed.id;
438
+ if (!handler) {
439
+ this.#send({
440
+ kind: "reply",
441
+ id,
442
+ error: `No handler for "${parsed.type}".`
443
+ });
444
+ return;
445
+ }
446
+ (async () => {
447
+ try {
448
+ const failed = this.#send({
449
+ kind: "reply",
450
+ id,
451
+ result: await handler(parsed.data)
452
+ });
453
+ if (failed)
454
+ this.#send({
455
+ kind: "reply",
456
+ id,
457
+ error: `The reply could not be sent: ${failed.message}`
458
+ });
459
+ } catch (error) {
460
+ this.#send({
461
+ kind: "reply",
462
+ id,
463
+ error: error instanceof Error ? error.message : String(error)
464
+ });
465
+ }
466
+ })();
467
+ }
468
+ }
469
+ // packages/sharding/src/transport.ts
470
+ class BroadcastChannelTransport2 {
471
+ #channel;
472
+ constructor(name) {
473
+ this.#channel = new BroadcastChannel(name);
474
+ }
475
+ post(message) {
476
+ this.#channel.postMessage(message);
477
+ }
478
+ onMessage(listener) {
479
+ this.#channel.addEventListener("message", (event) => listener(event.data));
480
+ }
481
+ close() {
482
+ this.#channel.close();
483
+ }
484
+ }
485
+ var BUS_FRAME = "__lunibeeBus";
486
+ var PEER_DOWN_FRAME = "__lunibeePeerDown";
487
+ var LOCAL = new WeakMap;
488
+
489
+ class IpcTransport2 {
490
+ #endpoint;
491
+ #listener;
492
+ #message;
493
+ #down;
494
+ constructor(endpoint = process) {
495
+ if (typeof endpoint.send !== "function")
496
+ throw new Error("IpcTransport needs an IPC channel: run the shard in a process forked by ClusterManager.");
497
+ this.#endpoint = endpoint;
498
+ this.#listener = (frame) => {
499
+ const record = frame;
500
+ if (!record || typeof record !== "object")
501
+ return;
502
+ if (BUS_FRAME in record)
503
+ this.#message?.(record[BUS_FRAME]);
504
+ else if (PEER_DOWN_FRAME in record)
505
+ this.#down?.(record[PEER_DOWN_FRAME]);
506
+ };
507
+ endpoint.on("message", this.#listener);
508
+ let local = LOCAL.get(endpoint);
509
+ if (!local)
510
+ LOCAL.set(endpoint, local = new Set);
511
+ local.add(this);
512
+ }
513
+ post(message) {
514
+ this.#endpoint.send({ [BUS_FRAME]: message });
515
+ for (const peer of LOCAL.get(this.#endpoint) ?? [])
516
+ if (peer !== this) {
517
+ const copy = structuredClone(message);
518
+ queueMicrotask(() => peer.#message?.(copy));
519
+ }
520
+ }
521
+ onMessage(listener) {
522
+ this.#message = listener;
523
+ }
524
+ onPeerDown(listener) {
525
+ this.#down = listener;
526
+ }
527
+ close() {
528
+ this.#endpoint.off("message", this.#listener);
529
+ LOCAL.get(this.#endpoint)?.delete(this);
530
+ }
531
+ }
532
+ function defaultTransport(name) {
533
+ if (process.env.LUNIBEE_SHARD_BUS === "ipc") {
534
+ if (typeof process.send === "function")
535
+ return new IpcTransport2;
536
+ const thread = threadEndpoint2();
537
+ if (thread)
538
+ return new IpcTransport2(thread);
539
+ }
540
+ return new BroadcastChannelTransport2(name);
541
+ }
542
+
543
+ // packages/sharding/src/bus.ts
544
+ var REPLY_TYPE = "__lunibee:reply";
545
+ var MAX_NAME_LENGTH = 256;
546
+ function isShardMessage(message) {
547
+ if (!message || typeof message !== "object")
548
+ return false;
549
+ const m = message;
550
+ return typeof m.type === "string" && m.type.length > 0 && m.type.length <= MAX_NAME_LENGTH && typeof m.id === "string" && m.id.length <= MAX_NAME_LENGTH && Number.isInteger(m.source) && m.source >= -1 && (m.target === null || Number.isInteger(m.target) && m.target >= -1);
551
+ }
552
+
553
+ class ShardBus2 {
554
+ #transport;
555
+ #maxPending;
556
+ #handlers = new Map;
557
+ #responders = new Map;
558
+ #errorHandlers = new Set;
559
+ #shardId;
560
+ #namespace;
561
+ channelName;
562
+ #counter = 0;
563
+ #pending = new Map;
564
+ #closed = false;
565
+ constructor(shardId, channelName, options = {}) {
566
+ if (!Number.isInteger(shardId) || shardId < 0)
567
+ throw new RangeError("Shard ID must be a non-negative integer.");
568
+ if (!channelName?.trim())
569
+ throw new TypeError("An application-specific shard channel name is required.");
570
+ this.#shardId = shardId;
571
+ this.#namespace = channelName.trim();
572
+ this.channelName = this.#namespace;
573
+ this.#maxPending = options.maxPending ?? 1e4;
574
+ this.#transport = options.transport ?? defaultTransport(this.channelName);
575
+ this.#transport.onMessage((message) => this.#dispatch(message));
576
+ this.#transport.onPeerDown?.((shardIds) => this.#peerDown(shardIds));
577
+ }
578
+ on(type, handler) {
579
+ if (!type.trim() || typeof handler !== "function")
580
+ throw new TypeError("Shard message type and handler are required.");
581
+ let handlers = this.#handlers.get(type);
582
+ if (!handlers)
583
+ this.#handlers.set(type, handlers = new Set);
584
+ handlers.add(handler);
585
+ return this;
586
+ }
587
+ off(type, handler) {
588
+ const handlers = this.#handlers.get(type);
589
+ handlers?.delete(handler);
590
+ const responder = this.#responders.get(type)?.get(handler);
591
+ if (responder) {
592
+ handlers?.delete(responder);
593
+ this.#responders.get(type).delete(handler);
594
+ }
595
+ return this;
596
+ }
597
+ onError(handler) {
598
+ if (typeof handler !== "function")
599
+ throw new TypeError("Shard bus error handler is required.");
600
+ this.#errorHandlers.add(handler);
601
+ return this;
602
+ }
603
+ offError(handler) {
604
+ this.#errorHandlers.delete(handler);
605
+ return this;
606
+ }
607
+ send(target, type, data) {
608
+ if (!Number.isInteger(target) || target < 0)
609
+ throw new RangeError("Shard target must be a non-negative integer.");
610
+ return this.#publish(target, type, data);
611
+ }
612
+ broadcast(type, data) {
613
+ return this.#publish(null, type, data);
614
+ }
615
+ respond(type, handler) {
616
+ const wrapper = async (message) => {
617
+ if (!message.expectsReply)
618
+ return;
619
+ const reply = { requestId: message.id };
620
+ try {
621
+ reply.result = await handler(message.data, message);
622
+ } catch (error) {
623
+ reply.error = error instanceof Error ? error.message : String(error);
624
+ }
625
+ this.#publish(message.source, REPLY_TYPE, reply);
626
+ };
627
+ let byType = this.#responders.get(type);
628
+ if (!byType)
629
+ this.#responders.set(type, byType = new Map);
630
+ byType.set(handler, wrapper);
631
+ return this.on(type, wrapper);
632
+ }
633
+ async request(target, type, data, timeoutMs = 5000) {
634
+ if (!Number.isInteger(target) || target < 0)
635
+ throw new RangeError("Shard target must be a non-negative integer.");
636
+ const [reply] = await this.#collect(target, type, data, timeoutMs, 1, true);
637
+ if (!reply)
638
+ throw new Error(`Shard ${target} did not reply to "${type}" within ${timeoutMs}ms.`);
639
+ if (reply.error !== undefined)
640
+ throw new Error(reply.error);
641
+ return reply.result;
642
+ }
643
+ broadcastRequest(type, data, options = {}) {
644
+ return this.#collect(null, type, data, options.timeoutMs ?? 5000, options.expected ?? Number.POSITIVE_INFINITY);
645
+ }
646
+ #collect(target, type, data, timeoutMs, expected, rejectOnPeerDown = false) {
647
+ const replies = [];
648
+ if (this.#closed)
649
+ return Promise.reject(new Error("ShardBus is closed."));
650
+ if (this.#pending.size >= this.#maxPending)
651
+ return Promise.reject(new Error(`Too many shard requests waiting for a reply (${this.#maxPending}).`));
652
+ return new Promise((resolve, reject) => {
653
+ let requestId = "";
654
+ const finish = () => {
655
+ clearTimeout(timer);
656
+ this.#pending.delete(requestId);
657
+ resolve(replies);
658
+ };
659
+ const timer = setTimeout(finish, timeoutMs);
660
+ requestId = this.#publish(target, type, data, true);
661
+ const fail = (error) => {
662
+ if (!rejectOnPeerDown)
663
+ return;
664
+ clearTimeout(timer);
665
+ this.#pending.delete(requestId);
666
+ reject(error);
667
+ };
668
+ this.#pending.set(requestId, {
669
+ target,
670
+ seen: new Set,
671
+ reply: (reply) => {
672
+ replies.push(reply);
673
+ if (replies.length >= expected)
674
+ finish();
675
+ },
676
+ fail,
677
+ cancel: (error) => rejectOnPeerDown ? fail(error) : finish()
678
+ });
679
+ });
680
+ }
681
+ close() {
682
+ if (this.#closed)
683
+ return;
684
+ this.#closed = true;
685
+ for (const pending of [...this.#pending.values()])
686
+ pending.cancel(new Error("ShardBus is closed."));
687
+ this.#pending.clear();
688
+ this.#transport.close();
689
+ this.#handlers.clear();
690
+ this.#errorHandlers.clear();
691
+ }
692
+ #publish(target, type, data, expectsReply = false) {
693
+ if (!type.trim())
694
+ throw new TypeError("Shard message type is required.");
695
+ if (this.#closed)
696
+ throw new Error("ShardBus is closed.");
697
+ const id = `${this.#namespace}:${this.#shardId}:${++this.#counter}`;
698
+ this.#transport.post({
699
+ source: this.#shardId,
700
+ target,
701
+ type,
702
+ data,
703
+ id,
704
+ ...expectsReply ? { expectsReply } : {}
705
+ });
706
+ return id;
707
+ }
708
+ #dispatch(message) {
709
+ if (this.#closed || !isShardMessage(message))
710
+ return;
711
+ if (message.source === this.#shardId || message.target !== null && message.target !== this.#shardId)
712
+ return;
713
+ if (message.type === REPLY_TYPE) {
714
+ const reply = message.data;
715
+ if (!reply || typeof reply !== "object" || typeof reply.requestId !== "string")
716
+ return;
717
+ const pending = this.#pending.get(reply.requestId);
718
+ if (!pending || pending.seen.has(message.source) || pending.target !== null && pending.target !== message.source)
719
+ return;
720
+ pending.seen.add(message.source);
721
+ pending.reply({
722
+ shardId: message.source,
723
+ ...reply.error !== undefined ? { error: String(reply.error) } : { result: reply.result }
724
+ });
725
+ return;
726
+ }
727
+ for (const handler of this.#handlers.get(message.type) ?? []) {
728
+ try {
729
+ const result = handler(message);
730
+ if (result && typeof result.then === "function")
731
+ Promise.resolve(result).catch((error) => this.#reportError(error, message));
732
+ } catch (error) {
733
+ this.#reportError(error, message);
734
+ }
735
+ }
736
+ }
737
+ #peerDown(shardIds) {
738
+ const down = new Set(shardIds);
739
+ for (const [id, pending] of this.#pending)
740
+ if (pending.target !== null && down.has(pending.target)) {
741
+ pending.fail(new Error(`Shard ${pending.target} went away before it replied.`));
742
+ this.#pending.delete(id);
743
+ }
744
+ }
745
+ #reportError(error, message) {
746
+ if (this.#errorHandlers.size === 0) {
747
+ process.emitWarning(error instanceof Error ? error : new Error(String(error)), { code: "LUNIBEE_SHARD_BUS_ERROR" });
748
+ return;
749
+ }
750
+ for (const handler of this.#errorHandlers) {
751
+ try {
752
+ handler(error, message);
753
+ } catch {}
754
+ }
755
+ }
756
+ }
757
+ // packages/sharding/src/supervisor.ts
758
+ class ShardSupervisor2 {
759
+ #options;
760
+ #restarts = new Map;
761
+ #random;
762
+ constructor(options = {}, random = Math.random) {
763
+ this.#options = {
764
+ restartDelay: options.restartDelay ?? 5000,
765
+ backoff: options.backoff ?? 1,
766
+ maxDelay: options.maxDelay ?? 300000,
767
+ jitter: options.jitter ?? 0,
768
+ maxRestarts: options.maxRestarts ?? Number.POSITIVE_INFINITY,
769
+ window: options.window ?? 60000
770
+ };
771
+ const { restartDelay, backoff, jitter, maxDelay, window, maxRestarts } = this.#options;
772
+ if (!(restartDelay >= 0) || !(maxDelay >= 0) || !(window > 0))
773
+ throw new RangeError("restartDelay and maxDelay cannot be negative, and window must be positive.");
774
+ if (!(backoff >= 1))
775
+ throw new RangeError("backoff must be at least 1.");
776
+ if (!(jitter >= 0 && jitter <= 1))
777
+ throw new RangeError("jitter must be between 0 and 1.");
778
+ if (!(maxRestarts >= 0))
779
+ throw new RangeError("maxRestarts cannot be negative.");
780
+ this.#random = random;
781
+ }
782
+ next(id, now = Date.now()) {
783
+ const { window, maxRestarts, restartDelay, backoff, maxDelay, jitter } = this.#options;
784
+ let history = this.#restarts.get(id);
785
+ if (history && now - history.restartAt >= window)
786
+ history = undefined;
787
+ const count = history?.count ?? 0;
788
+ if (count >= maxRestarts) {
789
+ this.#restarts.delete(id);
790
+ return null;
791
+ }
792
+ const base = Math.min(maxDelay, restartDelay * backoff ** count);
793
+ const delay = Math.min(maxDelay, Math.round(base * (1 + jitter * this.#random())));
794
+ this.#restarts.set(id, { count: count + 1, restartAt: now + delay });
795
+ return delay;
796
+ }
797
+ attempts(id) {
798
+ return this.#restarts.get(id)?.count ?? 0;
799
+ }
800
+ reset(id) {
801
+ if (id === undefined)
802
+ this.#restarts.clear();
803
+ else
804
+ this.#restarts.delete(id);
805
+ }
806
+ }
807
+ // packages/sharding/src/cluster.ts
808
+ import { EventEmitter as EventEmitter3 } from "events";
809
+ import { fork } from "child_process";
810
+ import { cpus } from "os";
811
+
812
+ // packages/sharding/src/identify.ts
813
+ var IDENTIFY_INTERVAL2 = 5000;
814
+
815
+ class IdentifyCoordinator2 {
816
+ maxConcurrency;
817
+ #interval;
818
+ #holdTimeout;
819
+ #now;
820
+ #schedule;
821
+ #cancel;
822
+ #onBudgetWait;
823
+ #onEvent;
824
+ #keys = new Map;
825
+ #remaining;
826
+ #total;
827
+ #resetAt = Number.POSITIVE_INFINITY;
828
+ #period = 24 * 60 * 60 * 1000;
829
+ #timer;
830
+ #timerAt = Number.POSITIVE_INFINITY;
831
+ #closed = false;
832
+ constructor(options = {}) {
833
+ const concurrency = options.maxConcurrency ?? 1;
834
+ if (!Number.isInteger(concurrency) || concurrency < 1)
835
+ throw new RangeError("maxConcurrency must be a positive integer.");
836
+ const interval = options.interval ?? IDENTIFY_INTERVAL2;
837
+ const hold = options.holdTimeout ?? 60000;
838
+ if (!(interval >= 0) || !(hold > 0))
839
+ throw new RangeError("interval cannot be negative and holdTimeout must be positive.");
840
+ this.maxConcurrency = concurrency;
841
+ this.#interval = interval;
842
+ this.#holdTimeout = hold;
843
+ this.#now = options.now ?? Date.now;
844
+ this.#schedule = options.schedule ?? ((callback, ms) => {
845
+ const timer = setTimeout(callback, ms);
846
+ timer.unref?.();
847
+ return timer;
848
+ });
849
+ this.#cancel = options.cancel ?? ((handle) => clearTimeout(handle));
850
+ this.#onBudgetWait = options.onBudgetWait;
851
+ this.#onEvent = options.onEvent;
852
+ if (options.budget) {
853
+ this.#total = options.budget.total;
854
+ this.#remaining = options.budget.remaining;
855
+ this.#resetAt = this.#now() + options.budget.resetAfter;
856
+ this.#period = options.budget.period ?? this.#period;
857
+ if (!(this.#period > 0))
858
+ throw new RangeError("budget.period must be positive.");
859
+ }
860
+ }
861
+ get remaining() {
862
+ return this.#remaining;
863
+ }
864
+ get waiting() {
865
+ let count = 0;
866
+ for (const key of this.#keys.values())
867
+ count += key.queue.length;
868
+ return count;
869
+ }
870
+ get holding() {
871
+ let count = 0;
872
+ for (const key of this.#keys.values())
873
+ if (key.holder)
874
+ count++;
875
+ return count;
876
+ }
877
+ acquire(owner, shardId) {
878
+ if (this.#closed)
879
+ return Promise.reject(new Error("IDENTIFY coordinator is closed."));
880
+ const index = shardId % this.maxConcurrency;
881
+ let key = this.#keys.get(index);
882
+ if (!key)
883
+ this.#keys.set(index, key = { queue: [], nextAt: 0 });
884
+ return new Promise((grant, fail) => {
885
+ key.queue.push({ owner, shardId, grant, fail, since: this.#now() });
886
+ this.#report({ type: "queued", owner, shardId, bucket: index });
887
+ this.pump();
888
+ });
889
+ }
890
+ release(owner, shardId) {
891
+ const key = this.#keys.get(shardId % this.maxConcurrency);
892
+ const holder = key?.holder;
893
+ if (!key || !holder || holder.owner !== owner || holder.shardId !== shardId)
894
+ return;
895
+ this.#free(key);
896
+ this.#report({
897
+ type: "released",
898
+ owner,
899
+ shardId,
900
+ bucket: shardId % this.maxConcurrency
901
+ });
902
+ this.pump();
903
+ }
904
+ #report(event) {
905
+ try {
906
+ this.#onEvent?.(event);
907
+ } catch {}
908
+ }
909
+ drop(owner, reason = "The cluster went away.") {
910
+ for (const key of this.#keys.values()) {
911
+ const keep = [];
912
+ for (const waiter of key.queue)
913
+ if (waiter.owner === owner) {
914
+ waiter.fail(new Error(reason));
915
+ this.#report({
916
+ type: "dropped",
917
+ owner,
918
+ shardId: waiter.shardId,
919
+ bucket: waiter.shardId % this.maxConcurrency
920
+ });
921
+ } else
922
+ keep.push(waiter);
923
+ key.queue = keep;
924
+ if (key.holder?.owner === owner)
925
+ this.#free(key);
926
+ }
927
+ this.pump();
928
+ }
929
+ close() {
930
+ this.#closed = true;
931
+ if (this.#timer !== undefined)
932
+ this.#cancel(this.#timer);
933
+ this.#timer = undefined;
934
+ this.#timerAt = Number.POSITIVE_INFINITY;
935
+ for (const key of this.#keys.values()) {
936
+ if (key.holder)
937
+ this.#cancel(key.holder.timer);
938
+ key.holder = undefined;
939
+ for (const waiter of key.queue)
940
+ waiter.fail(new Error("IDENTIFY coordinator is closed."));
941
+ key.queue = [];
942
+ }
943
+ }
944
+ pump() {
945
+ if (this.#closed)
946
+ return;
947
+ const now = this.#now();
948
+ if (this.#total !== undefined && now >= this.#resetAt) {
949
+ this.#remaining = this.#total;
950
+ while (now >= this.#resetAt)
951
+ this.#resetAt += this.#period;
952
+ }
953
+ let next = Number.POSITIVE_INFINITY;
954
+ for (const key of this.#keys.values()) {
955
+ if (key.holder || key.queue.length === 0)
956
+ continue;
957
+ if (key.nextAt > now) {
958
+ next = Math.min(next, key.nextAt);
959
+ continue;
960
+ }
961
+ if (this.#remaining !== undefined && this.#remaining <= 0) {
962
+ const waiter = key.queue[0];
963
+ this.#onBudgetWait?.({
964
+ shardId: waiter.shardId,
965
+ waitMs: this.#resetAt - now
966
+ });
967
+ next = Math.min(next, this.#resetAt);
968
+ continue;
969
+ }
970
+ const waiter = key.queue.shift();
971
+ if (this.#remaining !== undefined)
972
+ this.#remaining--;
973
+ const timer = this.#schedule(() => {
974
+ if (key.holder?.timer === timer) {
975
+ this.#report({
976
+ type: "expired",
977
+ owner: waiter.owner,
978
+ shardId: waiter.shardId,
979
+ bucket: waiter.shardId % this.maxConcurrency
980
+ });
981
+ this.#free(key);
982
+ this.pump();
983
+ }
984
+ }, this.#holdTimeout);
985
+ key.holder = {
986
+ owner: waiter.owner,
987
+ shardId: waiter.shardId,
988
+ timer
989
+ };
990
+ waiter.grant();
991
+ this.#report({
992
+ type: "granted",
993
+ owner: waiter.owner,
994
+ shardId: waiter.shardId,
995
+ bucket: waiter.shardId % this.maxConcurrency,
996
+ waitedMs: now - waiter.since
997
+ });
998
+ }
999
+ this.#arm(next);
1000
+ }
1001
+ #free(key) {
1002
+ if (key.holder)
1003
+ this.#cancel(key.holder.timer);
1004
+ key.holder = undefined;
1005
+ key.nextAt = this.#now() + this.#interval;
1006
+ }
1007
+ #arm(at) {
1008
+ if (at === this.#timerAt && this.#timer !== undefined)
1009
+ return;
1010
+ if (this.#timer !== undefined)
1011
+ this.#cancel(this.#timer);
1012
+ this.#timer = undefined;
1013
+ this.#timerAt = Number.POSITIVE_INFINITY;
1014
+ if (at === Number.POSITIVE_INFINITY)
1015
+ return;
1016
+ this.#timerAt = at;
1017
+ this.#timer = this.#schedule(() => {
1018
+ this.#timer = undefined;
1019
+ this.#timerAt = Number.POSITIVE_INFINITY;
1020
+ this.pump();
1021
+ }, Math.max(0, at - this.#now()));
1022
+ }
1023
+ }
1024
+
1025
+ // packages/sharding/src/remote.ts
1026
+ import { EventEmitter as EventEmitter2 } from "events";
1027
+ import { randomBytes } from "crypto";
1028
+
1029
+ // packages/sharding/src/remote-protocol.ts
1030
+ import { createHmac, timingSafeEqual } from "crypto";
1031
+ var REMOTE_PROTOCOL_VERSION2 = 1;
1032
+ var MAX_REMOTE_FRAME_BYTES = 8 * 1024 * 1024;
1033
+ var RemoteClose2 = {
1034
+ BadRequest: 4400,
1035
+ AuthFailed: 4401,
1036
+ HelloTimeout: 4408,
1037
+ DuplicateHost: 4409,
1038
+ TooManyConnections: 4429,
1039
+ UnknownHost: 4410,
1040
+ VersionMismatch: 4426,
1041
+ HostLost: 4504,
1042
+ Shutdown: 4500
1043
+ };
1044
+ var SPAWN_ENV_KEYS = new Set([
1045
+ "SHARD_LIST",
1046
+ "SHARD_COUNT",
1047
+ "CLUSTER_ID",
1048
+ "CLUSTER_COUNT",
1049
+ "LUNIBEE_SHARD_BUS",
1050
+ "LUNIBEE_CLUSTER_WORKER",
1051
+ "LUNIBEE_IDENTIFY",
1052
+ "LUNIBEE_STARTUP",
1053
+ "LUNIBEE_READY_TIMEOUT"
1054
+ ]);
1055
+ var KILL_SIGNALS = new Set([
1056
+ "SIGTERM",
1057
+ "SIGKILL",
1058
+ "SIGINT"
1059
+ ]);
1060
+ function isLoopbackHost(hostname) {
1061
+ return /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)$/i.test(hostname);
1062
+ }
1063
+ var HOST_ID = /^[A-Za-z0-9._-]{1,64}$/;
1064
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1065
+ var isInt = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
1066
+ function utf8Bytes(text) {
1067
+ return Buffer.byteLength(text, "utf8");
1068
+ }
1069
+ function parseJson(text, maxBytes) {
1070
+ if (typeof text !== "string")
1071
+ return null;
1072
+ if (text.length > maxBytes)
1073
+ return null;
1074
+ if (text.length * 3 > maxBytes && utf8Bytes(text) > maxBytes)
1075
+ return null;
1076
+ try {
1077
+ const value = JSON.parse(text);
1078
+ return isRecord2(value) ? value : null;
1079
+ } catch {
1080
+ return null;
1081
+ }
1082
+ }
1083
+ function signChallenge(secret, nonce, hostId) {
1084
+ return createHmac("sha256", secret).update(`${nonce}:${hostId}`).digest("hex");
1085
+ }
1086
+ function verifyChallenge(secret, nonce, hostId, mac) {
1087
+ if (typeof mac !== "string")
1088
+ return false;
1089
+ const expected = Buffer.from(signChallenge(secret, nonce, hostId), "hex");
1090
+ const given = Buffer.from(mac, "hex");
1091
+ return given.length === expected.length && timingSafeEqual(given, expected);
1092
+ }
1093
+ function parseToHost(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
1094
+ const v = parseJson(text, maxBytes);
1095
+ if (!v)
1096
+ return null;
1097
+ switch (v.t) {
1098
+ case "challenge":
1099
+ return typeof v.nonce === "string" && isInt(v.version) ? { t: "challenge", nonce: v.nonce, version: v.version } : null;
1100
+ case "welcome":
1101
+ return isInt(v.leaseMs) && isInt(v.pingMs) ? { t: "welcome", leaseMs: v.leaseMs, pingMs: v.pingMs } : null;
1102
+ case "ping":
1103
+ return isInt(v.n) ? { t: "ping", n: v.n } : null;
1104
+ case "spawn": {
1105
+ if (!isInt(v.cluster) || !isInt(v.epoch) || !isRecord2(v.env))
1106
+ return null;
1107
+ const env = {};
1108
+ for (const [key, value] of Object.entries(v.env)) {
1109
+ if (!SPAWN_ENV_KEYS.has(key) || typeof value !== "string")
1110
+ return null;
1111
+ env[key] = value;
1112
+ }
1113
+ return { t: "spawn", cluster: v.cluster, epoch: v.epoch, env };
1114
+ }
1115
+ case "frame":
1116
+ return isInt(v.cluster) && isInt(v.epoch) ? {
1117
+ t: "frame",
1118
+ cluster: v.cluster,
1119
+ epoch: v.epoch,
1120
+ frame: v.frame
1121
+ } : null;
1122
+ case "kill":
1123
+ return isInt(v.cluster) && isInt(v.epoch) && typeof v.signal === "string" && KILL_SIGNALS.has(v.signal) ? {
1124
+ t: "kill",
1125
+ cluster: v.cluster,
1126
+ epoch: v.epoch,
1127
+ signal: v.signal
1128
+ } : null;
1129
+ case "bye":
1130
+ return typeof v.reason === "string" ? { t: "bye", reason: v.reason } : null;
1131
+ default:
1132
+ return null;
1133
+ }
1134
+ }
1135
+ function parseToCoordinator(text, maxBytes = MAX_REMOTE_FRAME_BYTES) {
1136
+ const v = parseJson(text, maxBytes);
1137
+ if (!v)
1138
+ return null;
1139
+ switch (v.t) {
1140
+ case "hello":
1141
+ return typeof v.id === "string" && typeof v.mac === "string" && isInt(v.version) ? { t: "hello", id: v.id, mac: v.mac, version: v.version } : null;
1142
+ case "pong":
1143
+ return isInt(v.n) ? { t: "pong", n: v.n } : null;
1144
+ case "spawned":
1145
+ return isInt(v.cluster) && isInt(v.epoch) && (v.pid === undefined || isInt(v.pid)) ? {
1146
+ t: "spawned",
1147
+ cluster: v.cluster,
1148
+ epoch: v.epoch,
1149
+ ...v.pid !== undefined ? { pid: v.pid } : {}
1150
+ } : null;
1151
+ case "frame":
1152
+ return isInt(v.cluster) && isInt(v.epoch) ? {
1153
+ t: "frame",
1154
+ cluster: v.cluster,
1155
+ epoch: v.epoch,
1156
+ frame: v.frame
1157
+ } : null;
1158
+ case "exit":
1159
+ return isInt(v.cluster) && isInt(v.epoch) && (v.code === null || typeof v.code === "number") && (v.signal === null || typeof v.signal === "string") ? {
1160
+ t: "exit",
1161
+ cluster: v.cluster,
1162
+ epoch: v.epoch,
1163
+ code: v.code,
1164
+ signal: v.signal
1165
+ } : null;
1166
+ case "error":
1167
+ return isInt(v.cluster) && isInt(v.epoch) && typeof v.message === "string" ? {
1168
+ t: "error",
1169
+ cluster: v.cluster,
1170
+ epoch: v.epoch,
1171
+ message: v.message
1172
+ } : null;
1173
+ default:
1174
+ return null;
1175
+ }
1176
+ }
1177
+
1178
+ // packages/sharding/src/remote.ts
1179
+ class RemoteChild extends EventEmitter2 {
1180
+ host;
1181
+ cluster;
1182
+ epoch;
1183
+ pid;
1184
+ exitCode = null;
1185
+ signalCode = null;
1186
+ connected = true;
1187
+ #send;
1188
+ #kill;
1189
+ constructor(host, cluster, epoch, send, kill) {
1190
+ super();
1191
+ this.host = host;
1192
+ this.cluster = cluster;
1193
+ this.epoch = epoch;
1194
+ this.#send = send;
1195
+ this.#kill = kill;
1196
+ }
1197
+ send(frame) {
1198
+ return this.connected && this.exitCode === null && this.signalCode === null ? this.#send(frame) : false;
1199
+ }
1200
+ kill(signal = "SIGTERM") {
1201
+ if (this.exitCode !== null || this.signalCode !== null)
1202
+ return false;
1203
+ return this.#kill(signal);
1204
+ }
1205
+ spawned(pid) {
1206
+ this.pid = pid;
1207
+ }
1208
+ deliver(frame) {
1209
+ if (this.exitCode === null && this.signalCode === null)
1210
+ this.emit("message", frame);
1211
+ }
1212
+ finish(code, signal) {
1213
+ if (this.exitCode !== null || this.signalCode !== null)
1214
+ return;
1215
+ this.connected = false;
1216
+ this.exitCode = code ?? (signal === null ? 1 : null);
1217
+ this.signalCode = signal ?? null;
1218
+ this.emit("exit", code, signal);
1219
+ }
1220
+ }
1221
+
1222
+ class RemoteHosts {
1223
+ #options;
1224
+ #links = new Map;
1225
+ #droppedAt = new Map;
1226
+ #waiters = new Set;
1227
+ #hooks;
1228
+ #server;
1229
+ #watchdog;
1230
+ #epoch = 0;
1231
+ #ping = 0;
1232
+ #pending = new Set;
1233
+ #pendingByAddress = new Map;
1234
+ #rates = new Map;
1235
+ constructor(options, hooks) {
1236
+ if (typeof options.secret !== "string" || options.secret.length < 16)
1237
+ throw new RangeError("remote.secret must be a string of at least 16 characters.");
1238
+ if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)
1239
+ throw new RangeError("remote.port must be an integer from 0 to 65535.");
1240
+ if (options.tls && (typeof options.tls.cert !== "string" || typeof options.tls.key !== "string" || !options.tls.cert || !options.tls.key))
1241
+ throw new TypeError("remote.tls needs a non-empty cert and key.");
1242
+ if (!options.tls && !options.allowInsecure && !isLoopbackHost(options.hostname ?? "127.0.0.1"))
1243
+ throw new RangeError("remote listens on a network address without TLS: set remote.tls, bind 127.0.0.1 behind a TLS proxy, or set remote.allowInsecure for a network you fully trust.");
1244
+ const lease = options.lease ?? 15000;
1245
+ if (!Number.isFinite(lease) || lease < 30)
1246
+ throw new RangeError("remote.lease must be at least 30 milliseconds.");
1247
+ for (const id of options.hosts ?? [])
1248
+ if (!HOST_ID.test(id))
1249
+ throw new RangeError(`"${id}" is not a valid host ID.`);
1250
+ if (options.hosts && (options.hosts.length === 0 || new Set(options.hosts).size !== options.hosts.length))
1251
+ throw new RangeError("remote.hosts must be unique and not empty.");
1252
+ if (options.minHosts !== undefined && (!Number.isInteger(options.minHosts) || options.minHosts < 1))
1253
+ throw new RangeError("remote.minHosts must be a positive integer.");
1254
+ this.#options = { ...options, lease };
1255
+ this.#hooks = hooks;
1256
+ }
1257
+ get port() {
1258
+ return this.#server?.port;
1259
+ }
1260
+ connected() {
1261
+ return [...this.#links.keys()].sort();
1262
+ }
1263
+ isConnected(hostId) {
1264
+ return this.#links.has(hostId);
1265
+ }
1266
+ fencedAt(hostId) {
1267
+ const dropped = this.#droppedAt.get(hostId);
1268
+ return dropped === undefined || this.#links.has(hostId) ? 0 : dropped + this.#options.lease * 2;
1269
+ }
1270
+ start() {
1271
+ if (this.#server)
1272
+ return;
1273
+ const { port, hostname, tls, lease } = this.#options;
1274
+ this.#server = Bun.serve({
1275
+ port,
1276
+ hostname: hostname ?? "127.0.0.1",
1277
+ ...tls ? { tls } : {},
1278
+ fetch: (request, server) => {
1279
+ const peer = server.requestIP(request)?.address;
1280
+ const address = this.#options.clientAddress?.(request, peer) ?? peer ?? "unknown";
1281
+ if (!this.#allowConnection(address))
1282
+ return new Response("Too many connections.", {
1283
+ status: 429
1284
+ });
1285
+ if (server.upgrade(request, {
1286
+ data: {
1287
+ nonce: randomBytes(16).toString("hex"),
1288
+ address
1289
+ }
1290
+ }))
1291
+ return;
1292
+ return new Response("Lunibee cluster coordinator", {
1293
+ status: 426
1294
+ });
1295
+ },
1296
+ websocket: {
1297
+ maxPayloadLength: MAX_REMOTE_FRAME_BYTES,
1298
+ idleTimeout: 0,
1299
+ open: (ws) => this.#open(ws),
1300
+ message: (ws, raw) => this.#message(ws, raw),
1301
+ close: (ws, _code, reason) => this.#closed(ws, reason)
1302
+ }
1303
+ });
1304
+ const pingMs = Math.max(10, Math.floor(lease / 3));
1305
+ this.#watchdog = setInterval(() => this.#tick(), pingMs);
1306
+ this.#watchdog.unref?.();
1307
+ }
1308
+ stop() {
1309
+ if (this.#watchdog)
1310
+ clearInterval(this.#watchdog);
1311
+ this.#watchdog = undefined;
1312
+ for (const link of [...this.#links.values()]) {
1313
+ this.#post(link, { t: "bye", reason: "The coordinator stopped." });
1314
+ this.#drop(link, "The coordinator stopped.", RemoteClose2.Shutdown);
1315
+ }
1316
+ this.#server?.stop(true);
1317
+ this.#server = undefined;
1318
+ this.abortWaits();
1319
+ }
1320
+ waitFor(hosts, count, timeoutMs) {
1321
+ const done = () => hosts ? hosts.every((id) => this.#links.has(id)) : this.#links.size >= count;
1322
+ if (done())
1323
+ return Promise.resolve();
1324
+ return new Promise((resolve, reject) => {
1325
+ const waiter = {
1326
+ check: () => {
1327
+ if (!done())
1328
+ return;
1329
+ clearTimeout(timer);
1330
+ this.#waiters.delete(waiter);
1331
+ resolve();
1332
+ },
1333
+ abort: (error) => {
1334
+ clearTimeout(timer);
1335
+ this.#waiters.delete(waiter);
1336
+ reject(error);
1337
+ }
1338
+ };
1339
+ const timer = setTimeout(() => {
1340
+ const missing = hosts?.filter((id) => !this.#links.has(id));
1341
+ waiter.abort(new Error(hosts ? `Hosts did not join within ${timeoutMs}ms: [${missing}].` : `Only ${this.#links.size} of ${count} hosts joined within ${timeoutMs}ms.`));
1342
+ }, timeoutMs);
1343
+ this.#waiters.add(waiter);
1344
+ });
1345
+ }
1346
+ abortWaits() {
1347
+ for (const waiter of [...this.#waiters])
1348
+ waiter.abort(new Error("The coordinator stopped."));
1349
+ }
1350
+ status(declared = []) {
1351
+ const ids = new Set([...declared, ...this.#links.keys()]);
1352
+ return [...ids].sort().map((id) => {
1353
+ const link = this.#links.get(id);
1354
+ return {
1355
+ id,
1356
+ connected: link !== undefined,
1357
+ lastSeen: link?.lastSeen ?? 0,
1358
+ clusters: link ? [...link.children.keys()].sort((a, b) => a - b) : []
1359
+ };
1360
+ });
1361
+ }
1362
+ createChild(hostId, cluster, env) {
1363
+ const epoch = ++this.#epoch;
1364
+ const link = this.#links.get(hostId);
1365
+ const child = new RemoteChild(hostId, cluster, epoch, (frame) => link !== undefined && this.#post(link, { t: "frame", cluster, epoch, frame }), (signal) => link !== undefined && this.#post(link, { t: "kill", cluster, epoch, signal }));
1366
+ if (!link) {
1367
+ queueMicrotask(() => child.finish(null, "SIGHUP"));
1368
+ return child;
1369
+ }
1370
+ link.children.get(cluster)?.finish(null, "SIGKILL");
1371
+ link.children.set(cluster, child);
1372
+ if (!this.#post(link, { t: "spawn", cluster, epoch, env }))
1373
+ queueMicrotask(() => child.finish(null, "SIGHUP"));
1374
+ return child;
1375
+ }
1376
+ #post(link, message) {
1377
+ try {
1378
+ return link.ws.send(JSON.stringify(message)) > 0;
1379
+ } catch {
1380
+ return false;
1381
+ }
1382
+ }
1383
+ #allowConnection(address) {
1384
+ const rate = this.#options.connectionsPerSecond ?? 10;
1385
+ const now = Date.now();
1386
+ let bucket = this.#rates.get(address);
1387
+ if (!bucket) {
1388
+ bucket = { tokens: rate, at: now };
1389
+ this.#rates.set(address, bucket);
1390
+ if (this.#rates.size > 1e4)
1391
+ this.#rates.delete(this.#rates.keys().next().value);
1392
+ }
1393
+ bucket.tokens = Math.min(rate, bucket.tokens + (now - bucket.at) / 1000 * rate);
1394
+ bucket.at = now;
1395
+ if (bucket.tokens < 1)
1396
+ return false;
1397
+ bucket.tokens--;
1398
+ return true;
1399
+ }
1400
+ #evict(victim) {
1401
+ this.#uncount(victim);
1402
+ clearTimeout(victim.data.helloTimer);
1403
+ victim.close(RemoteClose2.TooManyConnections, "Too many connections.");
1404
+ }
1405
+ #open(ws) {
1406
+ const address = ws.data.address;
1407
+ if ((this.#pendingByAddress.get(address) ?? 0) >= (this.#options.maxUnauthenticatedPerAddress ?? 4)) {
1408
+ for (const other of this.#pending)
1409
+ if (other.data.address === address) {
1410
+ this.#evict(other);
1411
+ break;
1412
+ }
1413
+ }
1414
+ if (this.#pending.size >= (this.#options.maxUnauthenticated ?? 16)) {
1415
+ const oldest = this.#pending.values().next().value;
1416
+ if (oldest)
1417
+ this.#evict(oldest);
1418
+ }
1419
+ this.#pending.add(ws);
1420
+ this.#pendingByAddress.set(address, (this.#pendingByAddress.get(address) ?? 0) + 1);
1421
+ ws.data.counted = true;
1422
+ ws.data.helloTimer = setTimeout(() => ws.close(RemoteClose2.HelloTimeout, "No hello."), this.#options.helloTimeout ?? 3000);
1423
+ ws.send(JSON.stringify({
1424
+ t: "challenge",
1425
+ nonce: ws.data.nonce,
1426
+ version: REMOTE_PROTOCOL_VERSION2
1427
+ }));
1428
+ }
1429
+ #message(ws, raw) {
1430
+ if (typeof raw !== "string") {
1431
+ ws.close(RemoteClose2.BadRequest, "Text frames only.");
1432
+ return;
1433
+ }
1434
+ const message = parseToCoordinator(raw);
1435
+ if (!message) {
1436
+ ws.close(RemoteClose2.BadRequest, "Malformed frame.");
1437
+ return;
1438
+ }
1439
+ if (ws.data.id === undefined) {
1440
+ this.#hello(ws, message);
1441
+ return;
1442
+ }
1443
+ const link = this.#links.get(ws.data.id);
1444
+ if (!link || link.ws !== ws)
1445
+ return;
1446
+ link.lastSeen = Date.now();
1447
+ if (message.t === "pong" || message.t === "hello")
1448
+ return;
1449
+ const child = link.children.get(message.cluster);
1450
+ if (!child || child.epoch !== message.epoch)
1451
+ return;
1452
+ switch (message.t) {
1453
+ case "spawned":
1454
+ child.spawned(message.pid);
1455
+ break;
1456
+ case "frame":
1457
+ child.deliver(message.frame);
1458
+ break;
1459
+ case "exit":
1460
+ link.children.delete(message.cluster);
1461
+ child.finish(message.code, message.signal);
1462
+ break;
1463
+ case "error":
1464
+ this.#hooks.error(new Error(`Host ${link.id} could not run cluster ${message.cluster}: ${message.message}`));
1465
+ link.children.delete(message.cluster);
1466
+ child.finish(1, null);
1467
+ break;
1468
+ }
1469
+ }
1470
+ #hello(ws, message) {
1471
+ if (message?.t !== "hello") {
1472
+ ws.close(RemoteClose2.BadRequest, "Expected hello.");
1473
+ return;
1474
+ }
1475
+ const { id } = message;
1476
+ if (message.version !== REMOTE_PROTOCOL_VERSION2) {
1477
+ ws.close(RemoteClose2.VersionMismatch, "Protocol version mismatch.");
1478
+ return;
1479
+ }
1480
+ if (!HOST_ID.test(id) || !verifyChallenge(this.#options.secret, ws.data.nonce, id, message.mac)) {
1481
+ ws.close(RemoteClose2.AuthFailed, "Authentication failed.");
1482
+ return;
1483
+ }
1484
+ if (this.#options.hosts && !this.#options.hosts.includes(id)) {
1485
+ ws.close(RemoteClose2.UnknownHost, "Host is not part of this deployment.");
1486
+ return;
1487
+ }
1488
+ if (this.#links.has(id)) {
1489
+ ws.close(RemoteClose2.DuplicateHost, "A host with this ID is connected.");
1490
+ return;
1491
+ }
1492
+ clearTimeout(ws.data.helloTimer);
1493
+ this.#uncount(ws);
1494
+ ws.data.id = id;
1495
+ const link = {
1496
+ id,
1497
+ ws,
1498
+ lastSeen: Date.now(),
1499
+ children: new Map
1500
+ };
1501
+ this.#links.set(id, link);
1502
+ this.#droppedAt.delete(id);
1503
+ const lease = this.#options.lease;
1504
+ this.#post(link, {
1505
+ t: "welcome",
1506
+ leaseMs: lease,
1507
+ pingMs: Math.max(10, Math.floor(lease / 3))
1508
+ });
1509
+ this.#hooks.join(id);
1510
+ for (const waiter of [...this.#waiters])
1511
+ waiter.check();
1512
+ }
1513
+ #uncount(ws) {
1514
+ if (!ws.data.counted)
1515
+ return;
1516
+ ws.data.counted = false;
1517
+ this.#pending.delete(ws);
1518
+ const left = (this.#pendingByAddress.get(ws.data.address) ?? 1) - 1;
1519
+ if (left > 0)
1520
+ this.#pendingByAddress.set(ws.data.address, left);
1521
+ else
1522
+ this.#pendingByAddress.delete(ws.data.address);
1523
+ }
1524
+ #closed(ws, reason) {
1525
+ clearTimeout(ws.data.helloTimer);
1526
+ this.#uncount(ws);
1527
+ const link = ws.data.id ? this.#links.get(ws.data.id) : undefined;
1528
+ if (link && link.ws === ws)
1529
+ this.#drop(link, reason || "The connection closed.");
1530
+ }
1531
+ #drop(link, reason, code) {
1532
+ if (this.#links.get(link.id) !== link)
1533
+ return;
1534
+ this.#links.delete(link.id);
1535
+ this.#droppedAt.set(link.id, Date.now());
1536
+ if (this.#droppedAt.size > 1000)
1537
+ this.#droppedAt.delete(this.#droppedAt.keys().next().value);
1538
+ if (code !== undefined)
1539
+ try {
1540
+ link.ws.close(code, reason.slice(0, 100));
1541
+ } catch {}
1542
+ for (const child of [...link.children.values()])
1543
+ child.finish(null, "SIGHUP");
1544
+ link.children.clear();
1545
+ this.#hooks.leave(link.id, reason);
1546
+ }
1547
+ #tick() {
1548
+ const now = Date.now();
1549
+ for (const link of [...this.#links.values()]) {
1550
+ if (now - link.lastSeen > this.#options.lease * 2) {
1551
+ this.#drop(link, "The host stopped answering.", RemoteClose2.HostLost);
1552
+ continue;
1553
+ }
1554
+ this.#post(link, { t: "ping", n: ++this.#ping });
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ // packages/sharding/src/requests.ts
1560
+ class RequestTracker2 {
1561
+ #pending = new Map;
1562
+ #maxPending;
1563
+ #prefix;
1564
+ #counter = 0;
1565
+ constructor(prefix = "parent", maxPending = 1e4) {
1566
+ if (!Number.isInteger(maxPending) || maxPending < 1)
1567
+ throw new RangeError("maxPending must be a positive integer.");
1568
+ this.#prefix = prefix;
1569
+ this.#maxPending = maxPending;
1570
+ }
1571
+ get size() {
1572
+ return this.#pending.size;
1573
+ }
1574
+ begin(options) {
1575
+ if (this.#pending.size >= this.#maxPending)
1576
+ throw new Error(`Too many requests waiting for a reply (${this.#maxPending}).`);
1577
+ const id = `${this.#prefix}:${++this.#counter}`;
1578
+ const done = new Promise((resolve) => {
1579
+ const finish = () => {
1580
+ const entry = this.#pending.get(id);
1581
+ if (!entry)
1582
+ return;
1583
+ this.#pending.delete(id);
1584
+ clearTimeout(entry.timer);
1585
+ entry.cleanup?.();
1586
+ resolve({
1587
+ replies: entry.replies,
1588
+ missing: [...entry.keys.keys()].filter((key) => !entry.answered.has(key))
1589
+ });
1590
+ };
1591
+ const entry = {
1592
+ keys: new Map(options.keys),
1593
+ replies: [],
1594
+ answered: new Set,
1595
+ finish,
1596
+ timer: setTimeout(finish, options.timeoutMs)
1597
+ };
1598
+ if (options.signal) {
1599
+ const signal = options.signal;
1600
+ if (signal.aborted)
1601
+ queueMicrotask(finish);
1602
+ else {
1603
+ signal.addEventListener("abort", finish, { once: true });
1604
+ entry.cleanup = () => signal.removeEventListener("abort", finish);
1605
+ }
1606
+ }
1607
+ this.#pending.set(id, entry);
1608
+ if (entry.keys.size === 0)
1609
+ queueMicrotask(finish);
1610
+ });
1611
+ return { id, done };
1612
+ }
1613
+ answer(id, key, from, value) {
1614
+ const entry = this.#pending.get(id);
1615
+ if (!entry || entry.keys.get(key) !== from)
1616
+ return false;
1617
+ if (entry.answered.has(key))
1618
+ return false;
1619
+ entry.answered.add(key);
1620
+ entry.replies.push({ key, ...value });
1621
+ if (entry.replies.length >= entry.keys.size)
1622
+ entry.finish();
1623
+ return true;
1624
+ }
1625
+ failOwner(owner, error) {
1626
+ for (const entry of [...this.#pending.values()]) {
1627
+ for (const [key, keyOwner] of entry.keys)
1628
+ if (keyOwner === owner && !entry.answered.has(key)) {
1629
+ entry.answered.add(key);
1630
+ entry.replies.push({ key, error });
1631
+ }
1632
+ if (entry.replies.length >= entry.keys.size)
1633
+ entry.finish();
1634
+ }
1635
+ }
1636
+ close() {
1637
+ for (const entry of [...this.#pending.values()])
1638
+ entry.finish();
1639
+ }
1640
+ }
1641
+
1642
+ // packages/sharding/src/cluster.ts
1643
+ function notStarted() {
1644
+ return Object.assign(new EventEmitter3, {
1645
+ exitCode: 1,
1646
+ signalCode: null,
1647
+ connected: false,
1648
+ pid: undefined,
1649
+ kill: () => false
1650
+ });
1651
+ }
1652
+ var DEFAULT_MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
1653
+ function payloadSize(value) {
1654
+ if (value === undefined || value === null)
1655
+ return 0;
1656
+ if (typeof value === "string")
1657
+ return Buffer.byteLength(value, "utf8");
1658
+ if (typeof value !== "object")
1659
+ return String(value).length;
1660
+ try {
1661
+ const text = JSON.stringify(value);
1662
+ return text === undefined ? 0 : Buffer.byteLength(text, "utf8");
1663
+ } catch {
1664
+ return Number.POSITIVE_INFINITY;
1665
+ }
1666
+ }
1667
+ var isLive = (state) => state === "starting" || state === "ready";
1668
+
1669
+ class ClusterManager2 {
1670
+ clusters = new Map;
1671
+ shardCount = 0;
1672
+ #options;
1673
+ #auto;
1674
+ #autoScaleTimer;
1675
+ #infos = new Map;
1676
+ #owner = new Map;
1677
+ #startPromise;
1678
+ #stopPromise;
1679
+ #wasStarted = false;
1680
+ #stopping = false;
1681
+ #generation = 0;
1682
+ #leading = false;
1683
+ #renewTimer;
1684
+ #lastRenew = 0;
1685
+ #renewing = false;
1686
+ #term = 0;
1687
+ #restartTimers = new Set;
1688
+ #fencing = new Map;
1689
+ #startTimers = new Map;
1690
+ #pauses = new Set;
1691
+ #readyWaiters = new Set;
1692
+ #supervisor;
1693
+ #tracker;
1694
+ #coordinator;
1695
+ #allReady = false;
1696
+ #clusterCount = 0;
1697
+ #identifyWaiting = new Map;
1698
+ #remote;
1699
+ #hostOf = new Map;
1700
+ #lastWait = "";
1701
+ #listeners = new Map;
1702
+ constructor(options) {
1703
+ if (!options.token?.trim())
1704
+ throw new TypeError("A Discord bot token is required.");
1705
+ if (!options.script?.trim())
1706
+ throw new TypeError("A script path is required for clustering.");
1707
+ if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000)) {
1708
+ throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
1709
+ }
1710
+ if (options.clusterCount !== undefined && options.shardsPerCluster !== undefined)
1711
+ throw new RangeError("Set either clusterCount or shardsPerCluster, not both.");
1712
+ for (const [name, value] of [
1713
+ ["clusterCount", options.clusterCount],
1714
+ ["shardsPerCluster", options.shardsPerCluster],
1715
+ ["maxConcurrency", options.maxConcurrency]
1716
+ ])
1717
+ if (value !== undefined && (!Number.isInteger(value) || value < 1))
1718
+ throw new RangeError(`${name} must be a positive integer.`);
1719
+ if (options.maxMessageBytes !== undefined && (!Number.isInteger(options.maxMessageBytes) || options.maxMessageBytes < 1))
1720
+ throw new RangeError("maxMessageBytes must be a positive integer.");
1721
+ for (const [name, value] of [
1722
+ ["spawnInterval", options.spawnInterval],
1723
+ ["startTimeout", options.startTimeout],
1724
+ ["requestTimeout", options.requestTimeout]
1725
+ ])
1726
+ if (value !== undefined && !(value >= 0))
1727
+ throw new RangeError(`${name} cannot be negative.`);
1728
+ if (options.startup !== undefined && options.startup !== "safe" && options.startup !== "sequential")
1729
+ throw new RangeError('startup must be "safe" or "sequential".');
1730
+ if (options.startup === "sequential" && options.identify?.coordinate === false)
1731
+ throw new RangeError('startup: "sequential" needs IDENTIFY coordination; remove identify.coordinate: false.');
1732
+ if (options.guildsPerShard !== undefined) {
1733
+ assertGuildsPerShard(options.guildsPerShard);
1734
+ if (typeof options.shardCount === "number")
1735
+ throw new RangeError('guildsPerShard needs shardCount: "auto".');
1736
+ }
1737
+ if (options.shardIds && typeof options.shardCount === "number")
1738
+ validateShardIds2(options.shardIds, options.shardCount);
1739
+ this.#auto = options.shardCount === "auto";
1740
+ const ttl = options.leader?.ttl;
1741
+ if (options.leader && (typeof options.leader.lock?.acquire !== "function" || typeof options.leader.lock.release !== "function"))
1742
+ throw new TypeError("leader.lock must have acquire() and release().");
1743
+ if (ttl !== undefined && (!Number.isInteger(ttl) || ttl < 100))
1744
+ throw new RangeError("leader.ttl must be an integer of at least 100.");
1745
+ this.#options = { ...options };
1746
+ this.#supervisor = new ShardSupervisor2({
1747
+ restartDelay: options.restartDelay,
1748
+ ...options.supervisor
1749
+ });
1750
+ this.#tracker = new RequestTracker2("parent", options.maxPendingRequests);
1751
+ if (options.remote)
1752
+ this.#remote = new RemoteHosts(options.remote, {
1753
+ join: (id) => this.#hostJoined(id),
1754
+ leave: (id, reason) => this.#emit("hostLeave", id, reason),
1755
+ error: (error) => this.#emit("error", error)
1756
+ });
1757
+ }
1758
+ on(event, listener) {
1759
+ let set = this.#listeners.get(event);
1760
+ if (!set)
1761
+ this.#listeners.set(event, set = new Set);
1762
+ set.add(listener);
1763
+ return this;
1764
+ }
1765
+ off(event, listener) {
1766
+ this.#listeners.get(event)?.delete(listener);
1767
+ return this;
1768
+ }
1769
+ #emit(event, ...args) {
1770
+ for (const listener of this.#listeners.get(event) ?? [])
1771
+ try {
1772
+ listener(...args);
1773
+ } catch {}
1774
+ }
1775
+ async fetchRecommendedShardCount() {
1776
+ return resolveShardCount(this.#options.token, await fetchGatewayBot2(this.#options.token), this.#options);
1777
+ }
1778
+ clusterForShard(shardId) {
1779
+ const id = this.#owner.get(shardId);
1780
+ return id === undefined ? undefined : this.#infos.get(id);
1781
+ }
1782
+ assignments() {
1783
+ return new Map(this.#owner);
1784
+ }
1785
+ spawn() {
1786
+ if (isClusterWorker2())
1787
+ return Promise.reject(new Error("A cluster worker cannot start a ClusterManager: it would fork the whole fleet again."));
1788
+ if (this.#stopPromise)
1789
+ return this.#stopPromise.then(() => this.spawn());
1790
+ if (!this.#startPromise) {
1791
+ const attempt = this.#start();
1792
+ this.#startPromise = attempt;
1793
+ attempt.catch(() => {
1794
+ if (this.#startPromise === attempt)
1795
+ this.#startPromise = undefined;
1796
+ });
1797
+ }
1798
+ return this.#startPromise;
1799
+ }
1800
+ async#resolveGateway() {
1801
+ const { shardCount, maxConcurrency, fetchLimits } = this.#options;
1802
+ let info;
1803
+ let count;
1804
+ if (shardCount === "auto" || shardCount === undefined) {
1805
+ info = await fetchGatewayBot2(this.#options.token);
1806
+ count = await resolveShardCount(this.#options.token, info, this.#options);
1807
+ } else if (fetchLimits || this.#auto)
1808
+ try {
1809
+ info = await fetchGatewayBot2(this.#options.token);
1810
+ } catch (error) {
1811
+ this.#emit("error", error instanceof Error ? error : new Error(String(error)));
1812
+ }
1813
+ const limit = info?.sessionStartLimit;
1814
+ return {
1815
+ count: count ?? shardCount,
1816
+ maxConcurrency: Math.min(maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Number.POSITIVE_INFINITY),
1817
+ ...limit ? {
1818
+ budget: {
1819
+ total: limit.total,
1820
+ remaining: limit.remaining,
1821
+ resetAfter: limit.resetAfter
1822
+ }
1823
+ } : {}
1824
+ };
1825
+ }
1826
+ async#start() {
1827
+ const generation = this.#generation;
1828
+ const stale = () => this.#stopping || generation !== this.#generation;
1829
+ if (this.#options.leader && !this.#leading) {
1830
+ await this.#lead();
1831
+ if (stale())
1832
+ return;
1833
+ }
1834
+ const { count, maxConcurrency, budget } = await this.#resolveGateway();
1835
+ if (stale())
1836
+ return;
1837
+ const ids = this.#options.shardIds ?? Array.from({ length: count }, (_, i) => i);
1838
+ validateShardIds2(ids, count);
1839
+ const plan = planClusters2({
1840
+ shardIds: ids,
1841
+ clusterCount: this.#options.clusterCount,
1842
+ shardsPerCluster: this.#options.shardsPerCluster,
1843
+ defaultClusterCount: Math.max(1, cpus().length)
1844
+ });
1845
+ const chunks = plan.clusters;
1846
+ const placement = await this.#placeHosts().catch((error) => {
1847
+ if (stale())
1848
+ return;
1849
+ throw error;
1850
+ });
1851
+ if (!placement || stale())
1852
+ return;
1853
+ this.shardCount = count;
1854
+ this.#wasStarted = true;
1855
+ this.#owner.clear();
1856
+ this.#infos.clear();
1857
+ const identify = this.#options.identify;
1858
+ this.#coordinator = identify?.coordinate === false ? undefined : new IdentifyCoordinator2({
1859
+ maxConcurrency: this.#options.startup === "sequential" ? 1 : maxConcurrency,
1860
+ onEvent: (event) => this.#emit("identify", event),
1861
+ interval: identify?.interval,
1862
+ holdTimeout: identify?.holdTimeout,
1863
+ budget,
1864
+ onBudgetWait: (wait) => {
1865
+ const key = `${wait.shardId}:${Math.round(wait.waitMs / 1000)}`;
1866
+ if (key === this.#lastWait)
1867
+ return;
1868
+ this.#lastWait = key;
1869
+ this.#emit("identifyWait", wait);
1870
+ }
1871
+ });
1872
+ const active = chunks.map((shards, id) => ({ id, shards }));
1873
+ this.#clusterCount = active.length;
1874
+ this.#emit("plan", {
1875
+ totalShards: count,
1876
+ clusterCount: active.length,
1877
+ clusterCountSource: plan.source,
1878
+ requestedClusterCount: plan.requested,
1879
+ clusters: active.map((chunk) => ({
1880
+ id: chunk.id,
1881
+ shards: chunk.shards
1882
+ })),
1883
+ maxConcurrency,
1884
+ startup: this.#options.startup ?? "safe",
1885
+ identifyCoordinated: this.#coordinator !== undefined
1886
+ });
1887
+ this.#hostOf.clear();
1888
+ for (const chunk of active)
1889
+ this.#hostOf.set(chunk.id, placement[chunk.id % placement.length]);
1890
+ for (const [index, chunk] of active.entries()) {
1891
+ if (stale())
1892
+ return;
1893
+ for (const shard of chunk.shards)
1894
+ this.#owner.set(shard, chunk.id);
1895
+ this.#launchOrRecover(chunk.id, chunk.shards);
1896
+ const gap = this.#options.spawnInterval ?? 500;
1897
+ if (index < active.length - 1 && gap > 0)
1898
+ await this.#pause(gap);
1899
+ }
1900
+ if (stale())
1901
+ return;
1902
+ if (this.#options.waitForReady)
1903
+ await this.ready();
1904
+ if (this.#options.autoScaleInterval && this.#options.autoScaleInterval > 0) {
1905
+ this.#autoScaleTimer = setInterval(() => {
1906
+ this.checkAutoScale();
1907
+ }, this.#options.autoScaleInterval);
1908
+ }
1909
+ }
1910
+ get isLeader() {
1911
+ return !this.#options.leader || this.#leading;
1912
+ }
1913
+ async#lead() {
1914
+ const { lock, ttl = 1e4 } = this.#options.leader;
1915
+ const generation = this.#generation;
1916
+ while (!this.#stopping && generation === this.#generation) {
1917
+ const started = Date.now();
1918
+ let held = false;
1919
+ try {
1920
+ held = await withTimeout(lock.acquire(ttl), ttl / 2);
1921
+ } catch (error) {
1922
+ this.#emit("error", asError(error));
1923
+ }
1924
+ if (this.#stopping || generation !== this.#generation) {
1925
+ if (held)
1926
+ lock.release().catch(() => {
1927
+ return;
1928
+ });
1929
+ return;
1930
+ }
1931
+ if (held) {
1932
+ this.#leading = true;
1933
+ this.#lastRenew = started;
1934
+ this.#startRenewing(ttl);
1935
+ this.#emit("leaderAcquired");
1936
+ const lease = this.#options.remote ? this.#options.remote.lease ?? 15000 : 0;
1937
+ if (lease > 0)
1938
+ await this.#pause(lease * 2);
1939
+ return;
1940
+ }
1941
+ await this.#pause(Math.floor(ttl / 2));
1942
+ }
1943
+ }
1944
+ #startRenewing(ttl) {
1945
+ this.#stopRenewing();
1946
+ const lock = this.#options.leader.lock;
1947
+ const term = ++this.#term;
1948
+ this.#renewing = false;
1949
+ this.#renewTimer = setInterval(() => {
1950
+ if (Date.now() - this.#lastRenew > ttl / 2) {
1951
+ this.#standDown("The leader lock could not be renewed.");
1952
+ return;
1953
+ }
1954
+ if (this.#renewing)
1955
+ return;
1956
+ this.#renewing = true;
1957
+ const started = Date.now();
1958
+ withTimeout(lock.acquire(ttl), ttl / 4).then((held) => {
1959
+ if (term !== this.#term)
1960
+ return;
1961
+ this.#renewing = false;
1962
+ if (!this.#leading)
1963
+ return;
1964
+ if (held)
1965
+ this.#lastRenew = started;
1966
+ else
1967
+ this.#standDown("Another coordinator holds the leader lock.");
1968
+ }, (error) => {
1969
+ if (term !== this.#term)
1970
+ return;
1971
+ this.#renewing = false;
1972
+ this.#emit("error", asError(error));
1973
+ });
1974
+ }, Math.max(10, Math.floor(ttl / 4)));
1975
+ this.#renewTimer.unref?.();
1976
+ }
1977
+ #stopRenewing() {
1978
+ if (this.#renewTimer)
1979
+ clearInterval(this.#renewTimer);
1980
+ this.#renewTimer = undefined;
1981
+ }
1982
+ async#resign() {
1983
+ this.#stopRenewing();
1984
+ if (!this.#leading)
1985
+ return;
1986
+ this.#leading = false;
1987
+ const { lock, ttl = 1e4 } = this.#options.leader;
1988
+ try {
1989
+ await withTimeout(lock.release(), ttl / 2);
1990
+ } catch (error) {
1991
+ this.#emit("error", asError(error));
1992
+ }
1993
+ }
1994
+ #standDown(reason) {
1995
+ if (!this.#leading)
1996
+ return;
1997
+ this.#stopRenewing();
1998
+ this.#leading = false;
1999
+ this.#stopping = true;
2000
+ this.#halt();
2001
+ for (const cluster of this.#infos.values()) {
2002
+ cluster.state = "stopped";
2003
+ try {
2004
+ cluster.process.kill("SIGKILL");
2005
+ } catch {}
2006
+ }
2007
+ this.clusters.clear();
2008
+ this.#startPromise = undefined;
2009
+ this.#remote?.stop();
2010
+ this.#stopping = false;
2011
+ this.#emit("leaderLost", reason);
2012
+ this.spawn().catch((error) => this.#emit("error", asError(error)));
2013
+ }
2014
+ async#placeHosts() {
2015
+ const remote = this.#remote;
2016
+ const options = this.#options.remote;
2017
+ if (!remote || !options)
2018
+ return ["local"];
2019
+ remote.start();
2020
+ try {
2021
+ await remote.waitFor(options.hosts, options.minHosts ?? 1, options.joinTimeout ?? 60000);
2022
+ } catch (error) {
2023
+ remote.stop();
2024
+ throw error;
2025
+ }
2026
+ const ids = options.hosts ?? remote.connected();
2027
+ return options.local ? ["local", ...ids] : ids;
2028
+ }
2029
+ get remotePort() {
2030
+ return this.#remote?.port;
2031
+ }
2032
+ hosts() {
2033
+ return this.#remote?.status(this.#options.remote?.hosts) ?? [];
2034
+ }
2035
+ #pause(ms) {
2036
+ return new Promise((resolve) => {
2037
+ const wake = () => {
2038
+ clearTimeout(timer);
2039
+ this.#pauses.delete(wake);
2040
+ resolve();
2041
+ };
2042
+ const timer = setTimeout(wake, ms);
2043
+ this.#pauses.add(wake);
2044
+ });
2045
+ }
2046
+ #launch(id, shards) {
2047
+ const contract = {
2048
+ SHARD_LIST: shards.join(","),
2049
+ SHARD_COUNT: this.shardCount.toString(),
2050
+ CLUSTER_ID: id.toString(),
2051
+ CLUSTER_COUNT: this.#clusterCount.toString(),
2052
+ LUNIBEE_SHARD_BUS: "ipc",
2053
+ [WORKER_MARKER]: "1",
2054
+ LUNIBEE_IDENTIFY: this.#coordinator ? "coordinated" : "local",
2055
+ ...this.#options.startup === "sequential" ? {
2056
+ LUNIBEE_STARTUP: "sequential",
2057
+ LUNIBEE_READY_TIMEOUT: String(this.#options.identify?.holdTimeout ?? 60000)
2058
+ } : {}
2059
+ };
2060
+ const host = this.#hostOf.get(id) ?? "local";
2061
+ const env = {
2062
+ ...process.env,
2063
+ ...contract
2064
+ };
2065
+ const child = host !== "local" && this.#remote ? this.#remote.createChild(host, id, contract) : this.#options.fork ? this.#options.fork(this.#options.script, env) : fork(this.#options.script, [], {
2066
+ env
2067
+ });
2068
+ const known = this.#infos.get(id);
2069
+ const info = known ?? {
2070
+ id,
2071
+ host,
2072
+ process: child,
2073
+ shards,
2074
+ state: "starting",
2075
+ restarts: 0,
2076
+ startedAt: Date.now()
2077
+ };
2078
+ info.process = child;
2079
+ info.state = "starting";
2080
+ info.startedAt = Date.now();
2081
+ info.readyAt = undefined;
2082
+ this.#infos.set(id, info);
2083
+ this.clusters.set(id, info);
2084
+ this.#allReady = false;
2085
+ child.on("error", (error) => this.#emit("error", new Error(`Cluster ${id} process error: ${error.message}`, {
2086
+ cause: error
2087
+ })));
2088
+ child.on("message", (frame) => this.#onFrame(info, child, frame));
2089
+ child.once("exit", (code, signal) => this.#onExit(info, child, code, signal));
2090
+ const timeout = this.#options.startTimeout;
2091
+ if (timeout) {
2092
+ const timer = setTimeout(() => {
2093
+ this.#startTimers.delete(id);
2094
+ if (info.process !== child || info.state !== "starting")
2095
+ return;
2096
+ this.#emit("clusterStartTimeout", info);
2097
+ try {
2098
+ child.kill("SIGKILL");
2099
+ } catch {}
2100
+ }, timeout);
2101
+ timer.unref?.();
2102
+ this.#startTimers.set(id, timer);
2103
+ }
2104
+ this.#emit("clusterSpawn", info);
2105
+ return info;
2106
+ }
2107
+ #onExit(info, child, code, signal) {
2108
+ if (!this.#current(info, child)) {
2109
+ child.removeAllListeners("message");
2110
+ return;
2111
+ }
2112
+ child.removeAllListeners("message");
2113
+ this.#clearStartTimer(info.id);
2114
+ this.#coordinator?.drop(info.id, `Cluster ${info.id} exited.`);
2115
+ this.#identifyWaiting.delete(info.id);
2116
+ this.#tracker.failOwner(info.id, `Cluster ${info.id} exited.`);
2117
+ this.clusters.delete(info.id);
2118
+ this.#allReady = false;
2119
+ try {
2120
+ this.#options.onClusterExit?.(info, code, signal);
2121
+ } catch {}
2122
+ const expected = this.#stopping || info.state === "stopping" || info.state === "stopped";
2123
+ this.#emit("clusterExit", info, code, signal, expected);
2124
+ if (expected) {
2125
+ info.state = "stopped";
2126
+ return;
2127
+ }
2128
+ this.#announceDown(info.id, info.shards);
2129
+ this.#recover(info);
2130
+ }
2131
+ #launchOrRecover(id, shards) {
2132
+ try {
2133
+ this.#launch(id, shards);
2134
+ } catch (error) {
2135
+ const known = this.#infos.get(id);
2136
+ if (known)
2137
+ known.process = notStarted();
2138
+ const info = this.#infos.get(id) ?? {
2139
+ id,
2140
+ host: this.#hostOf.get(id) ?? "local",
2141
+ process: notStarted(),
2142
+ shards,
2143
+ state: "starting",
2144
+ restarts: 0,
2145
+ startedAt: Date.now()
2146
+ };
2147
+ this.#infos.set(id, info);
2148
+ this.clusters.delete(id);
2149
+ this.#allReady = false;
2150
+ this.#emit("error", new Error(`Cluster ${id} (shards ${shards.join(",")}) could not be spawned: ${error instanceof Error ? error.message : String(error)}`, { cause: error }));
2151
+ this.#recover(info);
2152
+ }
2153
+ }
2154
+ #recover(info) {
2155
+ if (this.#options.restartOnExit === false) {
2156
+ info.state = "failed";
2157
+ this.#rejectReady(info, "restartOnExit is false");
2158
+ return;
2159
+ }
2160
+ const delay = this.#supervisor.next(info.id);
2161
+ if (delay === null) {
2162
+ info.state = "failed";
2163
+ try {
2164
+ this.#options.onGiveUp?.(info);
2165
+ } catch {}
2166
+ this.#emit("clusterGiveUp", info);
2167
+ if (this.#options.reassignOnGiveUp)
2168
+ this.#reassign(info);
2169
+ else
2170
+ this.#rejectReady(info, "restarts exhausted");
2171
+ return;
2172
+ }
2173
+ info.state = "restarting";
2174
+ info.restarts++;
2175
+ this.#emit("clusterRestart", info, { delay, attempt: info.restarts });
2176
+ if (this.#supervisor.attempts(info.id) >= 3)
2177
+ this.#emit("clusterUnstable", info, {
2178
+ attempt: info.restarts,
2179
+ delay
2180
+ });
2181
+ this.#restartLater(info, delay);
2182
+ }
2183
+ #restartLater(info, delay) {
2184
+ const timer = setTimeout(() => {
2185
+ this.#restartTimers.delete(timer);
2186
+ this.#fencing.delete(info.id);
2187
+ if (this.#stopping || info.state !== "restarting")
2188
+ return;
2189
+ const wait = this.#relocate(info);
2190
+ if (wait > 0)
2191
+ this.#fencing.set(info.id, this.#restartLater(info, wait));
2192
+ else
2193
+ this.#launchOrRecover(info.id, info.shards);
2194
+ }, delay);
2195
+ timer.unref?.();
2196
+ this.#restartTimers.add(timer);
2197
+ return timer;
2198
+ }
2199
+ #hostJoined(hostId) {
2200
+ this.#emit("hostJoin", hostId);
2201
+ for (const [id, timer] of [...this.#fencing]) {
2202
+ const info = this.#infos.get(id);
2203
+ if (this.#hostOf.get(id) !== hostId || !info)
2204
+ continue;
2205
+ clearTimeout(timer);
2206
+ this.#restartTimers.delete(timer);
2207
+ this.#fencing.delete(id);
2208
+ if (!this.#stopping && info.state === "restarting")
2209
+ this.#launchOrRecover(info.id, info.shards);
2210
+ }
2211
+ }
2212
+ #relocate(info) {
2213
+ const remote = this.#remote;
2214
+ const host = this.#hostOf.get(info.id) ?? "local";
2215
+ if (!remote || this.#options.remote?.moveClusters === false || host === "local" || remote.isConnected(host))
2216
+ return 0;
2217
+ const wait = remote.fencedAt(host) - Date.now();
2218
+ if (wait > 0)
2219
+ return wait;
2220
+ const places = this.#options.remote?.local ? ["local", ...remote.connected()] : remote.connected();
2221
+ if (places.length === 0)
2222
+ return 0;
2223
+ const load = new Map(places.map((place) => [place, 0]));
2224
+ for (const [cluster, place] of this.#hostOf) {
2225
+ const state = this.#infos.get(cluster)?.state;
2226
+ if (load.has(place) && state !== "failed" && state !== "stopped")
2227
+ load.set(place, load.get(place) + 1);
2228
+ }
2229
+ const target = places.reduce((best, place) => load.get(place) < load.get(best) ? place : best);
2230
+ this.#hostOf.set(info.id, target);
2231
+ info.host = target;
2232
+ this.#emit("clusterMoved", info, { from: host, to: target });
2233
+ return 0;
2234
+ }
2235
+ async#reassign(failed) {
2236
+ const shardIds = [...failed.shards];
2237
+ const target = [...this.#infos.values()].filter((cluster) => cluster.state === "ready").sort((a, b) => a.shards.length - b.shards.length)[0];
2238
+ if (!target) {
2239
+ this.#emit("error", new Error(`No ready cluster can take over shards [${shardIds}] of cluster ${failed.id}.`));
2240
+ this.#rejectReady(failed, "no ready cluster to take them");
2241
+ return;
2242
+ }
2243
+ try {
2244
+ await this.requestCluster(target.id, REQUEST_ASSIGN, { shardIds }, { timeoutMs: this.#options.startTimeout ?? 120000 });
2245
+ } catch (error) {
2246
+ this.#emit("error", new Error(`Cluster ${target.id} could not take over shards [${shardIds}]: ${error instanceof Error ? error.message : String(error)}`));
2247
+ this.#rejectReady(failed, "reassignment failed");
2248
+ return;
2249
+ }
2250
+ if (this.#stopping || failed.state !== "failed")
2251
+ return;
2252
+ for (const shard of shardIds)
2253
+ this.#owner.set(shard, target.id);
2254
+ target.shards.push(...shardIds);
2255
+ failed.shards = [];
2256
+ this.#emit("shardsReassigned", {
2257
+ shardIds,
2258
+ from: failed.id,
2259
+ to: target.id
2260
+ });
2261
+ this.#checkReady();
2262
+ }
2263
+ #clearStartTimer(id) {
2264
+ const timer = this.#startTimers.get(id);
2265
+ if (timer)
2266
+ clearTimeout(timer);
2267
+ this.#startTimers.delete(id);
2268
+ }
2269
+ #sendFrame(cluster, frame) {
2270
+ const child = cluster.process;
2271
+ if (!isLive(cluster.state) || typeof child.send !== "function" || !child.connected)
2272
+ return false;
2273
+ try {
2274
+ child.send(frame);
2275
+ return true;
2276
+ } catch {
2277
+ return false;
2278
+ }
2279
+ }
2280
+ #announceDown(exited, shards) {
2281
+ for (const [id, cluster] of this.clusters)
2282
+ if (id !== exited)
2283
+ this.#sendFrame(cluster, { [PEER_DOWN_FRAME]: shards });
2284
+ }
2285
+ #current(info, child) {
2286
+ return info.process === child && this.#infos.get(info.id) === info;
2287
+ }
2288
+ #onFrame(info, child, frame) {
2289
+ if (!this.#current(info, child))
2290
+ return;
2291
+ if (!frame || typeof frame !== "object")
2292
+ return;
2293
+ if (BUS_FRAME in frame) {
2294
+ this.#onBus(info, frame[BUS_FRAME]);
2295
+ return;
2296
+ }
2297
+ if (CLUSTER_FRAME in frame) {
2298
+ const body = parseChildFrame(frame[CLUSTER_FRAME]);
2299
+ if (body)
2300
+ this.#onControl(info, child, body);
2301
+ else
2302
+ this.#emit("error", new Error(`Cluster ${info.id} sent a malformed control frame.`));
2303
+ }
2304
+ }
2305
+ #onControl(info, child, frame) {
2306
+ switch (frame.kind) {
2307
+ case "ready": {
2308
+ if (!frame.shardIds.every((id) => info.shards.includes(id))) {
2309
+ this.#emit("error", new Error(`Cluster ${info.id} reported shards it does not own: [${frame.shardIds}].`));
2310
+ return;
2311
+ }
2312
+ this.#clearStartTimer(info.id);
2313
+ info.state = "ready";
2314
+ info.readyAt = info.lastReadyAt = Date.now();
2315
+ this.#emit("clusterReady", info);
2316
+ this.#checkReady();
2317
+ return;
2318
+ }
2319
+ case "identify": {
2320
+ const reply = (body) => {
2321
+ if (this.#current(info, child))
2322
+ this.#sendFrame(info, { [CLUSTER_FRAME]: body });
2323
+ };
2324
+ if (!this.#coordinator) {
2325
+ reply({
2326
+ kind: "identify-deny",
2327
+ id: frame.id,
2328
+ error: "IDENTIFY coordination is off."
2329
+ });
2330
+ return;
2331
+ }
2332
+ if (!info.shards.includes(frame.shardId)) {
2333
+ reply({
2334
+ kind: "identify-deny",
2335
+ id: frame.id,
2336
+ error: `Shard ${frame.shardId} does not belong to cluster ${info.id}.`
2337
+ });
2338
+ return;
2339
+ }
2340
+ let waiting = this.#identifyWaiting.get(info.id);
2341
+ if (!waiting)
2342
+ this.#identifyWaiting.set(info.id, waiting = new Set);
2343
+ if (waiting.has(frame.shardId)) {
2344
+ reply({
2345
+ kind: "identify-deny",
2346
+ id: frame.id,
2347
+ error: `Shard ${frame.shardId} is already waiting for a slot.`
2348
+ });
2349
+ return;
2350
+ }
2351
+ waiting.add(frame.shardId);
2352
+ const settled = () => {
2353
+ waiting.delete(frame.shardId);
2354
+ };
2355
+ this.#coordinator.acquire(info.id, frame.shardId).then(() => {
2356
+ settled();
2357
+ reply({ kind: "identify-grant", id: frame.id });
2358
+ }, (error) => {
2359
+ settled();
2360
+ reply({
2361
+ kind: "identify-deny",
2362
+ id: frame.id,
2363
+ error: error instanceof Error ? error.message : String(error)
2364
+ });
2365
+ });
2366
+ return;
2367
+ }
2368
+ case "identify-done":
2369
+ this.#coordinator?.release(info.id, frame.shardId);
2370
+ return;
2371
+ case "reply": {
2372
+ const tooBig = frame.error === undefined && this.#tooBig(frame.result);
2373
+ this.#tracker.answer(frame.id, `c:${info.id}`, info.id, {
2374
+ ...frame.error !== undefined ? { error: frame.error } : tooBig ? {
2375
+ error: `Cluster ${info.id} answered with more than ${this.#maxBytes} bytes.`
2376
+ } : { result: frame.result }
2377
+ });
2378
+ return;
2379
+ }
2380
+ case "health":
2381
+ if (this.#tooBig(frame.report)) {
2382
+ this.#emit("error", new Error(`Cluster ${info.id} sent a health report of more than ${this.#maxBytes} bytes.`));
2383
+ return;
2384
+ }
2385
+ info.report = frame.report;
2386
+ return;
2387
+ }
2388
+ }
2389
+ get #maxBytes() {
2390
+ return this.#options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
2391
+ }
2392
+ #tooBig(value) {
2393
+ return payloadSize(value) > this.#maxBytes;
2394
+ }
2395
+ #assertSize(data, what) {
2396
+ if (this.#tooBig(data))
2397
+ throw new RangeError(`${what} is larger than maxMessageBytes (${this.#maxBytes}).`);
2398
+ }
2399
+ #onBus(from, raw) {
2400
+ let message = raw;
2401
+ if (!message || typeof message !== "object" || typeof message.type !== "string" || message.type.length === 0 || message.type.length > 256 || message.id === undefined || typeof message.id !== "string" || message.id.length > 256 || !Number.isInteger(message.source) || message.target !== null && !Number.isInteger(message.target))
2402
+ return;
2403
+ if (!from.shards.includes(message.source))
2404
+ return;
2405
+ const { type, id, source, target, data, expectsReply } = message;
2406
+ message = {
2407
+ type,
2408
+ id,
2409
+ source,
2410
+ target,
2411
+ data,
2412
+ ...expectsReply === undefined ? {} : { expectsReply }
2413
+ };
2414
+ if (this.#tooBig(message.data)) {
2415
+ this.#emit("error", new Error(`Cluster ${from.id} sent a shard-bus message of more than ${this.#maxBytes} bytes; it was not relayed.`));
2416
+ return;
2417
+ }
2418
+ const frame = { [BUS_FRAME]: message };
2419
+ if (message.target === PARENT_ID) {
2420
+ const reply = message.data;
2421
+ if (message.type === REPLY_TYPE && reply && typeof reply.requestId === "string")
2422
+ this.#tracker.answer(reply.requestId, `s:${message.source}`, from.id, reply.error !== undefined ? { error: String(reply.error) } : { result: reply.result });
2423
+ return;
2424
+ }
2425
+ if (message.target === null) {
2426
+ for (const [id, cluster] of this.clusters)
2427
+ if (id !== from.id)
2428
+ this.#sendFrame(cluster, frame);
2429
+ return;
2430
+ }
2431
+ const owner = this.clusterForShard(message.target);
2432
+ if (!owner || owner.id === from.id)
2433
+ return;
2434
+ if (!this.#sendFrame(owner, frame))
2435
+ this.#sendFrame(from, {
2436
+ [PEER_DOWN_FRAME]: [message.target]
2437
+ });
2438
+ }
2439
+ async request(shardId, type, data, options = {}) {
2440
+ this.#assertSize(data, "The request data");
2441
+ const owner = this.clusterForShard(shardId);
2442
+ if (!owner)
2443
+ throw new RangeError(`Shard ${shardId} is not part of this deployment.`);
2444
+ const { id, done } = this.#tracker.begin({
2445
+ keys: new Map([[`s:${shardId}`, owner.id]]),
2446
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
2447
+ signal: options.signal
2448
+ });
2449
+ const sent = this.#sendFrame(owner, {
2450
+ [BUS_FRAME]: {
2451
+ source: PARENT_ID,
2452
+ target: shardId,
2453
+ type,
2454
+ data,
2455
+ id,
2456
+ expectsReply: true
2457
+ }
2458
+ });
2459
+ if (!sent) {
2460
+ this.#tracker.failOwner(owner.id, `Shard ${shardId} is down: cluster ${owner.id} is ${owner.state}.`);
2461
+ }
2462
+ const { replies } = await done;
2463
+ const reply = replies[0];
2464
+ if (!reply)
2465
+ throw new Error(`Shard ${shardId} did not reply to "${type}" in time.`);
2466
+ if (reply.error !== undefined)
2467
+ throw new Error(reply.error);
2468
+ return reply.result;
2469
+ }
2470
+ async broadcast(type, data, options = {}) {
2471
+ this.#assertSize(data, "The request data");
2472
+ const keys = new Map;
2473
+ const down = [];
2474
+ for (const [shard, clusterId] of this.#owner) {
2475
+ const cluster = this.#infos.get(clusterId);
2476
+ if (cluster && isLive(cluster.state))
2477
+ keys.set(`s:${shard}`, clusterId);
2478
+ else
2479
+ down.push(shard);
2480
+ }
2481
+ const { id, done } = this.#tracker.begin({
2482
+ keys,
2483
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
2484
+ signal: options.signal
2485
+ });
2486
+ const frame = {
2487
+ [BUS_FRAME]: {
2488
+ source: PARENT_ID,
2489
+ target: null,
2490
+ type,
2491
+ data,
2492
+ id,
2493
+ expectsReply: true
2494
+ }
2495
+ };
2496
+ for (const cluster of this.clusters.values())
2497
+ if (!this.#sendFrame(cluster, frame))
2498
+ this.#tracker.failOwner(cluster.id, `Cluster ${cluster.id} is ${cluster.state}.`);
2499
+ const { replies, missing } = await done;
2500
+ return {
2501
+ replies: replies.map((reply) => ({
2502
+ shardId: Number(reply.key.slice(2)),
2503
+ ...reply.error !== undefined ? { error: reply.error } : { result: reply.result }
2504
+ })),
2505
+ missing: [
2506
+ ...down,
2507
+ ...missing.map((key) => Number(key.slice(2)))
2508
+ ].sort((a, b) => a - b)
2509
+ };
2510
+ }
2511
+ async requestCluster(clusterId, type, data, options = {}) {
2512
+ this.#assertSize(data, "The request data");
2513
+ const cluster = this.#infos.get(clusterId);
2514
+ if (!cluster)
2515
+ throw new RangeError(`Cluster ${clusterId} does not exist.`);
2516
+ const { id, done } = this.#tracker.begin({
2517
+ keys: new Map([[`c:${clusterId}`, clusterId]]),
2518
+ timeoutMs: options.timeoutMs ?? this.#options.requestTimeout ?? 5000,
2519
+ signal: options.signal
2520
+ });
2521
+ if (!this.#sendFrame(cluster, {
2522
+ [CLUSTER_FRAME]: { kind: "request", id, type, data }
2523
+ }))
2524
+ this.#tracker.failOwner(clusterId, `Cluster ${clusterId} is ${cluster.state}.`);
2525
+ const { replies } = await done;
2526
+ const reply = replies[0];
2527
+ if (!reply)
2528
+ throw new Error(`Cluster ${clusterId} did not reply to "${type}" in time.`);
2529
+ if (reply.error !== undefined)
2530
+ throw new Error(reply.error);
2531
+ return reply.result;
2532
+ }
2533
+ ready(timeoutMs) {
2534
+ if (this.#isReady())
2535
+ return Promise.resolve();
2536
+ return new Promise((resolve, reject) => {
2537
+ const waiter = {
2538
+ resolve: () => {
2539
+ if (waiter.timer)
2540
+ clearTimeout(waiter.timer);
2541
+ this.#readyWaiters.delete(waiter);
2542
+ resolve();
2543
+ },
2544
+ reject: (error) => {
2545
+ if (waiter.timer)
2546
+ clearTimeout(waiter.timer);
2547
+ this.#readyWaiters.delete(waiter);
2548
+ reject(error);
2549
+ }
2550
+ };
2551
+ if (timeoutMs !== undefined)
2552
+ waiter.timer = setTimeout(() => waiter.reject(new Error(`Clusters were not all ready within ${timeoutMs}ms.`)), timeoutMs);
2553
+ this.#readyWaiters.add(waiter);
2554
+ });
2555
+ }
2556
+ #isReady() {
2557
+ let ready = 0;
2558
+ for (const cluster of this.#infos.values()) {
2559
+ if (cluster.state === "ready")
2560
+ ready++;
2561
+ else if (!(cluster.state === "failed" && cluster.shards.length === 0))
2562
+ return false;
2563
+ }
2564
+ return ready > 0;
2565
+ }
2566
+ #rejectReady(failed, why) {
2567
+ if (failed.shards.length === 0)
2568
+ return;
2569
+ const error = new Error(`Cluster ${failed.id} failed and shards [${failed.shards}] have no owner (${why}).`);
2570
+ for (const waiter of [...this.#readyWaiters])
2571
+ waiter.reject(error);
2572
+ }
2573
+ #checkReady() {
2574
+ if (!this.#isReady())
2575
+ return;
2576
+ for (const waiter of [...this.#readyWaiters])
2577
+ waiter.resolve();
2578
+ if (!this.#allReady) {
2579
+ this.#allReady = true;
2580
+ this.#emit("ready");
2581
+ }
2582
+ }
2583
+ health() {
2584
+ const now = Date.now();
2585
+ const clusters = [...this.#infos.values()].sort((a, b) => a.id - b.id).map((cluster) => ({
2586
+ id: cluster.id,
2587
+ state: cluster.state,
2588
+ ...cluster.process.pid !== undefined && isLive(cluster.state) ? { pid: cluster.process.pid } : {},
2589
+ host: cluster.host,
2590
+ shardIds: [...cluster.shards],
2591
+ uptimeMs: isLive(cluster.state) ? now - cluster.startedAt : 0,
2592
+ restarts: cluster.restarts,
2593
+ startedAt: cluster.startedAt,
2594
+ ...cluster.readyAt !== undefined ? { readyAt: cluster.readyAt } : {},
2595
+ ...cluster.lastReadyAt !== undefined ? { lastReadyAt: cluster.lastReadyAt } : {},
2596
+ shards: cluster.report?.shards ?? [],
2597
+ ...cluster.report?.metrics ? { metrics: cluster.report.metrics } : {},
2598
+ ...cluster.report ? { reportedAt: cluster.report.reportedAt } : {}
2599
+ }));
2600
+ const missingShards = [...this.#owner].filter(([, id]) => this.#infos.get(id)?.state !== "ready").map(([shard]) => shard).sort((a, b) => a - b);
2601
+ const states = clusters.map((cluster) => cluster.state);
2602
+ const state = this.#stopping ? "stopping" : clusters.length === 0 ? this.#wasStarted ? "stopped" : "idle" : states.every((s) => s === "stopped") ? "stopped" : states.every((s) => s === "ready") ? "ready" : states.some((s) => s === "restarting" || s === "failed") ? "degraded" : "starting";
2603
+ return {
2604
+ state,
2605
+ totalShards: this.shardCount,
2606
+ clusters,
2607
+ readyClusters: states.filter((s) => s === "ready").length,
2608
+ missingShards
2609
+ };
2610
+ }
2611
+ async refreshHealth(options = {}) {
2612
+ await Promise.all([...this.clusters.values()].map(async (cluster) => {
2613
+ try {
2614
+ cluster.report = await this.requestCluster(cluster.id, REQUEST_HEALTH, { metrics: options.metrics === true }, { timeoutMs: options.timeoutMs ?? 2000 });
2615
+ } catch {}
2616
+ }));
2617
+ return this.health();
2618
+ }
2619
+ async checkAutoScale() {
2620
+ if (!this.#auto)
2621
+ return;
2622
+ try {
2623
+ const recommended = await this.fetchRecommendedShardCount();
2624
+ if (recommended !== this.shardCount) {
2625
+ await this.respawn(recommended);
2626
+ }
2627
+ } catch (error) {
2628
+ const normalized = error instanceof Error ? error : new Error(String(error));
2629
+ this.#options.onAutoScaleError?.(normalized);
2630
+ }
2631
+ }
2632
+ async respawn(newShardCount) {
2633
+ this.#options.shardCount = newShardCount;
2634
+ await this.#stop(this.#options.shutdownTimeout ?? 5000, true);
2635
+ this.#supervisor.reset();
2636
+ await this.spawn();
2637
+ }
2638
+ shutdownAll(timeoutMs = this.#options.shutdownTimeout ?? 5000) {
2639
+ return this.#stop(timeoutMs, false);
2640
+ }
2641
+ #stop(timeoutMs, keepRemote) {
2642
+ this.#stopPromise ??= this.#shutdown(timeoutMs, keepRemote).finally(() => {
2643
+ this.#stopPromise = undefined;
2644
+ });
2645
+ return this.#stopPromise;
2646
+ }
2647
+ async#shutdown(timeoutMs, keepRemote) {
2648
+ this.#stopping = true;
2649
+ this.#halt();
2650
+ await this.#startPromise?.catch(() => {
2651
+ return;
2652
+ });
2653
+ const started = Date.now();
2654
+ const live = [...this.clusters.values()];
2655
+ await Promise.all(live.map((cluster) => this.#shutdownCluster(cluster, timeoutMs)));
2656
+ this.#emit("shutdown", {
2657
+ clusters: live.length,
2658
+ durationMs: Date.now() - started
2659
+ });
2660
+ for (const cluster of this.#infos.values())
2661
+ cluster.state = "stopped";
2662
+ this.clusters.clear();
2663
+ this.#startPromise = undefined;
2664
+ if (!keepRemote) {
2665
+ this.#remote?.stop();
2666
+ await this.#resign();
2667
+ }
2668
+ this.#stopping = false;
2669
+ }
2670
+ #halt() {
2671
+ this.#generation++;
2672
+ for (const timer of this.#restartTimers)
2673
+ clearTimeout(timer);
2674
+ this.#restartTimers.clear();
2675
+ this.#fencing.clear();
2676
+ for (const id of [...this.#startTimers.keys()])
2677
+ this.#clearStartTimer(id);
2678
+ for (const wake of [...this.#pauses])
2679
+ wake();
2680
+ if (this.#autoScaleTimer) {
2681
+ clearInterval(this.#autoScaleTimer);
2682
+ this.#autoScaleTimer = undefined;
2683
+ }
2684
+ this.#coordinator?.close();
2685
+ this.#coordinator = undefined;
2686
+ this.#remote?.abortWaits();
2687
+ this.#tracker.close();
2688
+ for (const waiter of [...this.#readyWaiters])
2689
+ waiter.reject(new Error("The cluster manager is stopping."));
2690
+ }
2691
+ async#shutdownCluster(cluster, timeoutMs) {
2692
+ const child = cluster.process;
2693
+ cluster.state = "stopping";
2694
+ if (child.exitCode !== null || child.signalCode !== null)
2695
+ return;
2696
+ await new Promise((resolve) => {
2697
+ let timer;
2698
+ const finish = () => {
2699
+ if (timer)
2700
+ clearTimeout(timer);
2701
+ resolve();
2702
+ };
2703
+ child.once("exit", finish);
2704
+ try {
2705
+ child.kill("SIGTERM");
2706
+ } catch {
2707
+ child.off("exit", finish);
2708
+ finish();
2709
+ return;
2710
+ }
2711
+ timer = setTimeout(() => {
2712
+ try {
2713
+ child.kill("SIGKILL");
2714
+ } catch {}
2715
+ timer = setTimeout(finish, Math.max(timeoutMs, 1000));
2716
+ }, timeoutMs);
2717
+ });
2718
+ }
2719
+ killAll() {
2720
+ this.#stopping = true;
2721
+ this.#halt();
2722
+ for (const cluster of this.#infos.values()) {
2723
+ cluster.state = "stopped";
2724
+ try {
2725
+ cluster.process.kill();
2726
+ } catch {}
2727
+ }
2728
+ this.clusters.clear();
2729
+ this.#startPromise = undefined;
2730
+ this.#remote?.stop();
2731
+ this.#resign();
2732
+ this.#stopping = false;
2733
+ }
2734
+ }
2735
+ function withTimeout(promise, ms) {
2736
+ let timer;
2737
+ const timeout = new Promise((_, reject) => {
2738
+ timer = setTimeout(() => reject(new Error(`The leader lock did not answer within ${ms} ms.`)), ms);
2739
+ });
2740
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
2741
+ }
2742
+ function asError(error) {
2743
+ return error instanceof Error ? error : new Error(String(error));
2744
+ }
2745
+ // packages/sharding/src/host.ts
2746
+ import { fork as fork2 } from "child_process";
2747
+ class ClusterHost2 {
2748
+ #options;
2749
+ #running = new Map;
2750
+ #listeners = new Map;
2751
+ #socket;
2752
+ #urls;
2753
+ #urlIndex = 0;
2754
+ #unreachable = 0;
2755
+ #accepted = false;
2756
+ #lease = 15000;
2757
+ #lastRx = 0;
2758
+ #watchdog;
2759
+ #retry;
2760
+ #delay;
2761
+ #busyAttempts = 0;
2762
+ #closed = true;
2763
+ #connected = false;
2764
+ #firstConnect;
2765
+ constructor(options) {
2766
+ if (!HOST_ID.test(options.id))
2767
+ throw new RangeError("id must be 1-64 letters, digits, '.', '_' or '-'.");
2768
+ if (!options.secret || options.secret.length < 16)
2769
+ throw new RangeError("secret must be at least 16 characters.");
2770
+ if (!options.script?.trim())
2771
+ throw new TypeError("A worker script path is required.");
2772
+ const urls = Array.isArray(options.url) ? [...options.url] : [options.url];
2773
+ if (urls.length === 0 || urls.length > 16)
2774
+ throw new TypeError("url must list 1 to 16 coordinators.");
2775
+ for (const raw of urls) {
2776
+ let url;
2777
+ try {
2778
+ url = new URL(raw);
2779
+ } catch {
2780
+ throw new TypeError("url must be an absolute ws:// or wss:// URL.");
2781
+ }
2782
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
2783
+ throw new TypeError("url must start with ws:// or wss://.");
2784
+ if (url.username || url.password)
2785
+ throw new TypeError("url must not contain credentials.");
2786
+ if (url.protocol === "ws:" && !options.allowInsecure && !isLoopbackHost(url.hostname))
2787
+ throw new TypeError("url must use wss:// to reach another machine (allowInsecure: true overrides this for a network you fully trust).");
2788
+ }
2789
+ this.#urls = urls;
2790
+ this.#options = options;
2791
+ this.#delay = options.reconnectDelay ?? 1000;
2792
+ }
2793
+ get connected() {
2794
+ return this.#connected;
2795
+ }
2796
+ clusters() {
2797
+ return [...this.#running.keys()].sort((a, b) => a - b);
2798
+ }
2799
+ on(event, listener) {
2800
+ let set = this.#listeners.get(event);
2801
+ if (!set)
2802
+ this.#listeners.set(event, set = new Set);
2803
+ set.add(listener);
2804
+ return this;
2805
+ }
2806
+ off(event, listener) {
2807
+ this.#listeners.get(event)?.delete(listener);
2808
+ return this;
2809
+ }
2810
+ #emit(event, ...args) {
2811
+ for (const listener of this.#listeners.get(event) ?? [])
2812
+ try {
2813
+ listener(...args);
2814
+ } catch {}
2815
+ }
2816
+ connect() {
2817
+ if (!this.#closed)
2818
+ return Promise.resolve();
2819
+ this.#closed = false;
2820
+ this.#busyAttempts = 0;
2821
+ this.#unreachable = 0;
2822
+ return new Promise((resolve, reject) => {
2823
+ this.#firstConnect = { resolve, reject };
2824
+ this.#open();
2825
+ });
2826
+ }
2827
+ close() {
2828
+ this.#closed = true;
2829
+ if (this.#retry)
2830
+ clearTimeout(this.#retry);
2831
+ this.#retry = undefined;
2832
+ this.#fence();
2833
+ if (this.#watchdog)
2834
+ clearInterval(this.#watchdog);
2835
+ this.#watchdog = undefined;
2836
+ this.#connected = false;
2837
+ const socket = this.#socket;
2838
+ this.#socket = undefined;
2839
+ this.#firstConnect?.reject(new Error("The host was closed."));
2840
+ this.#firstConnect = undefined;
2841
+ if (!socket || socket.readyState === WebSocket.CLOSED)
2842
+ return Promise.resolve();
2843
+ return new Promise((resolve) => {
2844
+ socket.addEventListener("close", () => resolve(), { once: true });
2845
+ try {
2846
+ socket.close(1000, "Host closed.");
2847
+ } catch {
2848
+ resolve();
2849
+ }
2850
+ });
2851
+ }
2852
+ #open() {
2853
+ this.#accepted = false;
2854
+ const socket = new WebSocket(this.#urls[this.#urlIndex]);
2855
+ this.#socket = socket;
2856
+ socket.addEventListener("message", (event) => this.#message(socket, event.data));
2857
+ socket.addEventListener("error", () => {});
2858
+ socket.addEventListener("close", (event) => {
2859
+ if (this.#socket === socket)
2860
+ this.#lost(event.code, event.reason);
2861
+ });
2862
+ }
2863
+ #send(socket, message) {
2864
+ if (socket.readyState !== WebSocket.OPEN)
2865
+ return false;
2866
+ try {
2867
+ socket.send(JSON.stringify(message));
2868
+ return true;
2869
+ } catch {
2870
+ return false;
2871
+ }
2872
+ }
2873
+ #refuseSpawn(socket, data) {
2874
+ if (typeof data !== "string" || data.length > 65536)
2875
+ return;
2876
+ let frame;
2877
+ try {
2878
+ frame = JSON.parse(data);
2879
+ } catch {
2880
+ return;
2881
+ }
2882
+ if (frame?.t !== "spawn" || !Number.isInteger(frame.cluster) || !Number.isInteger(frame.epoch))
2883
+ return;
2884
+ this.#send(socket, {
2885
+ t: "error",
2886
+ cluster: frame.cluster,
2887
+ epoch: frame.epoch,
2888
+ message: "This host refused the spawn request (malformed or not allowed)."
2889
+ });
2890
+ }
2891
+ #message(socket, data) {
2892
+ if (this.#socket !== socket)
2893
+ return;
2894
+ const message = parseToHost(data);
2895
+ if (!message) {
2896
+ this.#emit("error", new Error("The coordinator sent a malformed frame."));
2897
+ this.#refuseSpawn(socket, data);
2898
+ return;
2899
+ }
2900
+ this.#lastRx = Date.now();
2901
+ switch (message.t) {
2902
+ case "challenge":
2903
+ if (message.version !== REMOTE_PROTOCOL_VERSION2) {
2904
+ socket.close(RemoteClose2.VersionMismatch, "Protocol version mismatch.");
2905
+ return;
2906
+ }
2907
+ this.#send(socket, {
2908
+ t: "hello",
2909
+ id: this.#options.id,
2910
+ mac: signChallenge(this.#options.secret, message.nonce, this.#options.id),
2911
+ version: REMOTE_PROTOCOL_VERSION2
2912
+ });
2913
+ return;
2914
+ case "welcome":
2915
+ this.#lease = message.leaseMs;
2916
+ this.#accepted = true;
2917
+ this.#connected = true;
2918
+ this.#delay = this.#options.reconnectDelay ?? 1000;
2919
+ this.#startWatchdog();
2920
+ this.#firstConnect?.resolve();
2921
+ this.#firstConnect = undefined;
2922
+ this.#emit("connected");
2923
+ return;
2924
+ case "ping":
2925
+ this.#send(socket, { t: "pong", n: message.n });
2926
+ return;
2927
+ case "bye":
2928
+ this.#fence();
2929
+ return;
2930
+ case "spawn":
2931
+ this.#spawn(socket, message.cluster, message.epoch, message.env);
2932
+ return;
2933
+ case "frame": {
2934
+ const run = this.#running.get(message.cluster);
2935
+ if (run?.epoch === message.epoch && run.child.connected)
2936
+ try {
2937
+ run.child.send(message.frame);
2938
+ } catch {}
2939
+ return;
2940
+ }
2941
+ case "kill": {
2942
+ const run = this.#running.get(message.cluster);
2943
+ if (run?.epoch === message.epoch && KILL_SIGNALS.has(message.signal))
2944
+ try {
2945
+ run.child.kill(message.signal);
2946
+ } catch {}
2947
+ return;
2948
+ }
2949
+ }
2950
+ }
2951
+ #spawn(socket, cluster, epoch, env) {
2952
+ const reply = (message) => {
2953
+ if (this.#socket === socket)
2954
+ this.#send(socket, message);
2955
+ };
2956
+ if (this.#running.has(cluster)) {
2957
+ reply({
2958
+ t: "error",
2959
+ cluster,
2960
+ epoch,
2961
+ message: `Cluster ${cluster} is already running here.`
2962
+ });
2963
+ return;
2964
+ }
2965
+ let child;
2966
+ try {
2967
+ const forkWorker = this.#options.fork ?? defaultFork;
2968
+ child = forkWorker(this.#options.script, {
2969
+ ...process.env,
2970
+ ...env
2971
+ });
2972
+ } catch (error) {
2973
+ reply({
2974
+ t: "error",
2975
+ cluster,
2976
+ epoch,
2977
+ message: error instanceof Error ? error.message : String(error)
2978
+ });
2979
+ return;
2980
+ }
2981
+ this.#running.set(cluster, { epoch, child });
2982
+ child.on("message", (frame) => reply({ t: "frame", cluster, epoch, frame }));
2983
+ child.once("exit", (code, signal) => {
2984
+ if (this.#running.get(cluster)?.child === child)
2985
+ this.#running.delete(cluster);
2986
+ this.#emit("clusterExit", cluster, code, signal);
2987
+ reply({ t: "exit", cluster, epoch, code, signal });
2988
+ });
2989
+ child.on("error", (error) => this.#emit("error", error instanceof Error ? error : new Error(String(error))));
2990
+ reply({
2991
+ t: "spawned",
2992
+ cluster,
2993
+ epoch,
2994
+ ...child.pid ? { pid: child.pid } : {}
2995
+ });
2996
+ this.#emit("clusterSpawn", cluster);
2997
+ }
2998
+ #fence() {
2999
+ for (const { child } of [...this.#running.values()])
3000
+ try {
3001
+ child.kill("SIGKILL");
3002
+ } catch {}
3003
+ }
3004
+ #startWatchdog() {
3005
+ if (this.#watchdog)
3006
+ clearInterval(this.#watchdog);
3007
+ this.#lastRx = Date.now();
3008
+ this.#watchdog = setInterval(() => {
3009
+ if (Date.now() - this.#lastRx > this.#lease) {
3010
+ this.#fence();
3011
+ this.#socket?.close(RemoteClose2.HostLost, "Coordinator silent.");
3012
+ }
3013
+ }, Math.max(10, Math.floor(this.#lease / 3)));
3014
+ this.#watchdog.unref?.();
3015
+ }
3016
+ #lost(code, reason) {
3017
+ if (this.#watchdog)
3018
+ clearInterval(this.#watchdog);
3019
+ this.#watchdog = undefined;
3020
+ this.#socket = undefined;
3021
+ const wasConnected = this.#connected;
3022
+ this.#connected = false;
3023
+ this.#fence();
3024
+ if (wasConnected)
3025
+ this.#emit("disconnected", { code, reason });
3026
+ if (this.#closed)
3027
+ return;
3028
+ const refused = code === RemoteClose2.AuthFailed || code === RemoteClose2.UnknownHost || code === RemoteClose2.DuplicateHost || code === RemoteClose2.VersionMismatch;
3029
+ const first = this.#firstConnect;
3030
+ const busy = code === RemoteClose2.TooManyConnections || code === RemoteClose2.HelloTimeout;
3031
+ const accepted = this.#accepted;
3032
+ if (this.#urls.length > 1)
3033
+ this.#urlIndex = (this.#urlIndex + 1) % this.#urls.length;
3034
+ if (first && !refused && !busy && !accepted && ++this.#unreachable < this.#urls.length) {
3035
+ this.#open();
3036
+ return;
3037
+ }
3038
+ if (first && busy && ++this.#busyAttempts < (this.#options.connectAttempts ?? 10)) {
3039
+ this.#retryLater();
3040
+ return;
3041
+ }
3042
+ if (first) {
3043
+ this.#firstConnect = undefined;
3044
+ this.#closed = true;
3045
+ first.reject(new Error(`The coordinator refused this host (${code}${reason ? `: ${reason}` : ""}).`));
3046
+ return;
3047
+ }
3048
+ if (refused)
3049
+ this.#emit("error", new Error(`The coordinator refused this host (${code}: ${reason}).`));
3050
+ this.#retryLater();
3051
+ }
3052
+ #retryLater() {
3053
+ const wait = Math.round(this.#delay * (1 + Math.random() * 0.2));
3054
+ this.#delay = Math.min(this.#options.maxReconnectDelay ?? 30000, this.#delay * 2);
3055
+ this.#retry = setTimeout(() => {
3056
+ this.#retry = undefined;
3057
+ if (!this.#closed)
3058
+ this.#open();
3059
+ }, wait);
3060
+ }
3061
+ }
3062
+ function defaultFork(script, env) {
3063
+ return fork2(script, [], { env });
3064
+ }
3065
+ // packages/sharding/src/leader.ts
3066
+ var ACQUIRE = `local v = redis.call('GET', KEYS[1])
3067
+ if v == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end
3068
+ if not v then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return 1 end
3069
+ return 0`;
3070
+ var RELEASE = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end
3071
+ return 0`;
3072
+
3073
+ class RedisLeaderLock2 {
3074
+ #client;
3075
+ #key;
3076
+ #token;
3077
+ constructor(options) {
3078
+ if (typeof options?.client?.send !== "function")
3079
+ throw new TypeError("RedisLeaderLock needs a client with send().");
3080
+ this.#client = options.client;
3081
+ this.#key = options.key ?? "lunibee:cluster:leader";
3082
+ this.#token = options.token ?? crypto.randomUUID();
3083
+ if (!this.#key || !this.#token)
3084
+ throw new TypeError("key and token must not be empty.");
3085
+ }
3086
+ async acquire(ttlMs) {
3087
+ if (!Number.isInteger(ttlMs) || ttlMs < 1)
3088
+ throw new RangeError("ttlMs must be a positive integer.");
3089
+ const reply = await this.#client.send("EVAL", [
3090
+ ACQUIRE,
3091
+ "1",
3092
+ this.#key,
3093
+ this.#token,
3094
+ String(ttlMs)
3095
+ ]);
3096
+ return Number(reply) === 1;
3097
+ }
3098
+ async release() {
3099
+ await this.#client.send("EVAL", [RELEASE, "1", this.#key, this.#token]);
3100
+ }
3101
+ }
3102
+
3103
+ // packages/sharding/src/index.ts
3104
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3105
+ function needsConnection(gateway) {
3106
+ return gateway.state === "CONNECT" /* Connect */ || gateway.state === "RECONNECT" /* Reconnect */ || gateway.state === "CLOSED" /* Closed */;
3107
+ }
3108
+ var HANDSHAKE_SENT = new Set([
3109
+ "IDENTIFY" /* Identify */,
3110
+ "RESUME" /* Resume */,
3111
+ "READY" /* Ready */,
3112
+ "DISPATCH" /* Dispatch */,
3113
+ "HEARTBEAT" /* Heartbeat */,
3114
+ "CLOSED" /* Closed */
3115
+ ]);
3116
+ function handshakeSent(gateway, timeout) {
3117
+ if (HANDSHAKE_SENT.has(gateway.state))
3118
+ return Promise.resolve();
3119
+ return new Promise((resolve) => {
3120
+ const done = () => {
3121
+ clearTimeout(timer);
3122
+ gateway.off("stateChange", onState);
3123
+ resolve();
3124
+ };
3125
+ const onState = (change) => {
3126
+ if (HANDSHAKE_SENT.has(change.next))
3127
+ done();
3128
+ };
3129
+ const timer = setTimeout(done, timeout);
3130
+ gateway.on("stateChange", onState);
3131
+ });
3132
+ }
3133
+ function shardReady(gateway, timeout) {
3134
+ if (gateway.state === "DISPATCH" /* Dispatch */)
3135
+ return Promise.resolve(true);
3136
+ return new Promise((resolve) => {
3137
+ const done = (ready) => {
3138
+ clearTimeout(timer);
3139
+ gateway.off("ready", onReady);
3140
+ gateway.off("resumed", onReady);
3141
+ resolve(ready);
3142
+ };
3143
+ const onReady = () => done(true);
3144
+ const timer = setTimeout(() => done(false), timeout);
3145
+ gateway.on("ready", onReady);
3146
+ gateway.on("resumed", onReady);
3147
+ });
3148
+ }
3149
+ var SEQUENTIAL_READY_TIMEOUT2 = 60000;
3150
+ async function connectInQueue2(gateway, shardId, gate, options = {}) {
3151
+ await gate.acquire(shardId);
3152
+ try {
3153
+ if (options.cancelled?.())
3154
+ return false;
3155
+ await gateway.connect();
3156
+ await handshakeSent(gateway, options.handshakeTimeout ?? 15000);
3157
+ if (!options.waitForReady)
3158
+ return true;
3159
+ return await shardReady(gateway, options.readyTimeout ?? SEQUENTIAL_READY_TIMEOUT2);
3160
+ } finally {
3161
+ gate.release(shardId);
3162
+ }
3163
+ }
3164
+
3165
+ class ShardManager2 {
3166
+ static IDENTIFY_INTERVAL = 5000;
3167
+ shards = new Map;
3168
+ get shardCount() {
3169
+ return this.#resolved ? this.#total : 0;
3170
+ }
3171
+ get shardIds() {
3172
+ return [...this.shards.keys()].sort((a, b) => a - b);
3173
+ }
3174
+ #options;
3175
+ #ids;
3176
+ #gate;
3177
+ #worker;
3178
+ #lifecycle = new Map;
3179
+ #announced = false;
3180
+ #resolved = false;
3181
+ #destroyed = false;
3182
+ #generation = 0;
3183
+ #auto;
3184
+ #fixedCount;
3185
+ #startup;
3186
+ #readyTimeout;
3187
+ #autoScaleTimer;
3188
+ #total;
3189
+ #listeners = new Map;
3190
+ #gatewayInfo;
3191
+ constructor(options) {
3192
+ if (!options.token?.trim())
3193
+ throw new TypeError("A Discord bot token is required.");
3194
+ if (!Array.isArray(options.intents))
3195
+ throw new TypeError("Gateway intents must be an array, e.g. [GatewayIntentBits.Guilds].");
3196
+ const context = options.cluster === false ? null : options.cluster ?? readClusterContext2();
3197
+ if (context) {
3198
+ if (options.shardCount === "auto")
3199
+ throw new RangeError(`shardCount "auto" is the ClusterManager's decision inside a cluster worker.`);
3200
+ if (options.shardCount !== undefined && options.shardCount !== context.shardCount)
3201
+ throw new RangeError(`shardCount ${options.shardCount} differs from the cluster's ${context.shardCount}.`);
3202
+ if (options.autoScaleInterval !== undefined)
3203
+ throw new TypeError("autoScaleInterval belongs to ClusterManager inside a cluster worker.");
3204
+ if (options.shardIds && !options.shardIds.every((id) => context.shardIds.includes(id)))
3205
+ throw new RangeError("shardIds must be a subset of the cluster's own shards.");
3206
+ }
3207
+ const count = context ? context.shardCount : options.shardCount === "auto" || options.shardCount === undefined ? 1 : options.shardCount;
3208
+ if (!Number.isInteger(count) || count < 1)
3209
+ throw new RangeError("Shard count must be a positive integer.");
3210
+ if (options.guildsPerShard !== undefined) {
3211
+ assertGuildsPerShard(options.guildsPerShard);
3212
+ if (options.shardCount !== "auto")
3213
+ throw new RangeError('guildsPerShard needs shardCount: "auto".');
3214
+ }
3215
+ this.#ids = options.shardIds ?? context?.shardIds;
3216
+ if (this.#ids && options.shardCount !== "auto")
3217
+ validateShardIds2(this.#ids, count);
3218
+ this.#worker = options.worker === false ? undefined : options.worker ?? (context && options.cluster === undefined ? ClusterWorker2.current() : undefined);
3219
+ this.#gate = options.identify === false ? undefined : options.identify ?? (context?.coordinatedIdentify ? this.#worker?.identifyGate : undefined);
3220
+ if (options.autoScaleInterval !== undefined && (!Number.isInteger(options.autoScaleInterval) || options.autoScaleInterval < 1000))
3221
+ throw new RangeError("autoScaleInterval must be an integer of at least 1000 milliseconds.");
3222
+ if (options.spawnDelay !== undefined && (!Number.isFinite(options.spawnDelay) || options.spawnDelay < 0))
3223
+ throw new RangeError("spawnDelay must be a non-negative finite number of milliseconds.");
3224
+ if (options.maxConcurrency !== undefined && (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency < 1))
3225
+ throw new RangeError("maxConcurrency must be a positive integer.");
3226
+ if (options.handshakeTimeout !== undefined && (!Number.isFinite(options.handshakeTimeout) || options.handshakeTimeout < 0))
3227
+ throw new RangeError("handshakeTimeout must be a non-negative number of milliseconds.");
3228
+ if (options.startup !== undefined && options.startup !== "safe" && options.startup !== "sequential")
3229
+ throw new RangeError('startup must be "safe" or "sequential".');
3230
+ this.#startup = options.startup ?? context?.startup ?? "safe";
3231
+ if (options.readyTimeout !== undefined && (!Number.isFinite(options.readyTimeout) || options.readyTimeout <= 0))
3232
+ throw new RangeError("readyTimeout must be a positive number of milliseconds.");
3233
+ this.#readyTimeout = options.readyTimeout ?? context?.readyTimeout ?? SEQUENTIAL_READY_TIMEOUT2;
3234
+ this.#total = count;
3235
+ this.#fixedCount = count;
3236
+ this.#auto = options.shardCount === "auto";
3237
+ this.#options = {
3238
+ spawnDelay: ShardManager2.IDENTIFY_INTERVAL,
3239
+ ...options
3240
+ };
3241
+ this.#worker?.onRequest(REQUEST_HEALTH, (data) => this.report(data?.metrics === true));
3242
+ this.#worker?.onRequest(REQUEST_ASSIGN, async (data) => {
3243
+ const ids = data?.shardIds;
3244
+ if (!Array.isArray(ids))
3245
+ throw new TypeError("shardIds must be an array.");
3246
+ await this.addShards(ids);
3247
+ return { shardIds: ids };
3248
+ });
3249
+ if (options.shardCount !== "auto")
3250
+ this.#initialize(count);
3251
+ }
3252
+ async fetchRecommendedShardCount() {
3253
+ return resolveShardCount(this.#options.token, await this.fetchGatewayInfo(), this.#options);
3254
+ }
3255
+ async fetchGatewayInfo() {
3256
+ const info = await fetchGatewayBot2(this.#options.token);
3257
+ this.#gatewayInfo = info;
3258
+ return info;
3259
+ }
3260
+ async connect() {
3261
+ if (this.#autoScaleTimer) {
3262
+ clearInterval(this.#autoScaleTimer);
3263
+ this.#autoScaleTimer = undefined;
3264
+ }
3265
+ await this.#ensureInitialized();
3266
+ await this.#start([...this.shards].filter(([, gateway]) => needsConnection(gateway)).map(([id]) => id));
3267
+ if (this.#options.autoScaleInterval && this.#auto && !this.#autoScaleTimer) {
3268
+ this.#autoScaleTimer = setInterval(() => {
3269
+ this.checkAutoScale();
3270
+ }, this.#options.autoScaleInterval);
3271
+ }
3272
+ }
3273
+ async addShards(shardIds) {
3274
+ await this.#ensureInitialized();
3275
+ validateShardIds2(shardIds, this.#total);
3276
+ for (const id of shardIds)
3277
+ if (this.shards.has(id))
3278
+ throw new RangeError(`Shard ${id} is already owned.`);
3279
+ for (const id of shardIds)
3280
+ this.shards.set(id, this.#createShard(id, this.#total));
3281
+ if (this.#ids)
3282
+ this.#ids = [...this.#ids, ...shardIds];
3283
+ this.#announced = false;
3284
+ try {
3285
+ await this.#start([...shardIds], false);
3286
+ } catch (error) {
3287
+ for (const id of shardIds) {
3288
+ this.shards.get(id)?.close();
3289
+ this.shards.delete(id);
3290
+ this.#lifecycle.delete(id);
3291
+ }
3292
+ if (this.#ids)
3293
+ this.#ids = this.#ids.filter((id) => !shardIds.includes(id));
3294
+ throw error;
3295
+ }
3296
+ }
3297
+ async#start(ids, destroyOnFailure = true) {
3298
+ const generation = this.#generation;
3299
+ const stale = () => generation !== this.#generation;
3300
+ const gateways = new Map(ids.map((id) => [id, this.shards.get(id)]));
3301
+ const gate = this.#gate;
3302
+ const sequential = this.#startup === "sequential";
3303
+ if (gate) {
3304
+ const results = await Promise.allSettled(ids.map(async (id) => {
3305
+ const gateway = gateways.get(id);
3306
+ this.#setStatus(id, "connecting");
3307
+ const ready = await connectInQueue2(gateway, id, gate, {
3308
+ handshakeTimeout: this.#options.handshakeTimeout,
3309
+ waitForReady: sequential,
3310
+ readyTimeout: this.#readyTimeout,
3311
+ cancelled: stale
3312
+ });
3313
+ if (!ready && sequential && !stale())
3314
+ this.#emit("shardError", id, new Error(`Shard ${id} was not READY within ${this.#readyTimeout} ms; the next shard starts.`));
3315
+ }));
3316
+ if (stale())
3317
+ return;
3318
+ const failed = results.findIndex((result) => result.status === "rejected");
3319
+ if (failed !== -1) {
3320
+ if (destroyOnFailure)
3321
+ this.destroy();
3322
+ throw new Error(`Failed to connect shard ${ids[failed]}.`, {
3323
+ cause: results[failed].reason
3324
+ });
3325
+ }
3326
+ return;
3327
+ }
3328
+ const limit = this.#gatewayInfo?.sessionStartLimit;
3329
+ if (limit && limit.remaining < ids.length)
3330
+ throw new Error(`Session start limit exhausted: ${limit.remaining} of ${limit.total} IDENTIFYs left for ${ids.length} shards; resets in ${Math.ceil(limit.resetAfter / 1000)}s.`);
3331
+ const concurrency = sequential ? 1 : Math.min(this.#options.maxConcurrency ?? limit?.maxConcurrency ?? 1, limit?.maxConcurrency ?? Infinity);
3332
+ const buckets = new Map;
3333
+ for (const id of ids) {
3334
+ const key = id % concurrency;
3335
+ const bucket = buckets.get(key);
3336
+ if (bucket)
3337
+ bucket.push(id);
3338
+ else
3339
+ buckets.set(key, [id]);
3340
+ }
3341
+ const rounds = Math.max(0, ...[...buckets.values()].map((bucket) => bucket.length));
3342
+ for (let index = 0;index < rounds; index++) {
3343
+ if (stale())
3344
+ return;
3345
+ const round = [...buckets.values()].map((bucket) => bucket[index]).filter((id) => id !== undefined);
3346
+ for (const id of round)
3347
+ this.#setStatus(id, "connecting");
3348
+ const results = await Promise.allSettled(round.map((id) => gateways.get(id).connect()));
3349
+ if (stale())
3350
+ return;
3351
+ const failed = results.findIndex((result) => result.status === "rejected");
3352
+ if (limit)
3353
+ limit.remaining = Math.max(0, limit.remaining - results.filter((r) => r.status === "fulfilled").length);
3354
+ if (failed !== -1) {
3355
+ if (destroyOnFailure)
3356
+ this.destroy();
3357
+ throw new Error(`Failed to connect shard ${round[failed]}.`, {
3358
+ cause: results[failed].reason
3359
+ });
3360
+ }
3361
+ if (sequential && index + 1 < rounds) {
3362
+ for (const id of round)
3363
+ if (!await shardReady(gateways.get(id), this.#readyTimeout) && !stale())
3364
+ this.#emit("shardError", id, new Error(`Shard ${id} was not READY within ${this.#readyTimeout} ms; the next shard starts.`));
3365
+ }
3366
+ if (index + 1 < rounds && this.#options.spawnDelay && this.#options.spawnDelay > 0) {
3367
+ await Promise.all(round.map((id) => handshakeSent(gateways.get(id), this.#options.handshakeTimeout ?? 15000)));
3368
+ await sleep(this.#options.spawnDelay);
3369
+ }
3370
+ }
3371
+ }
3372
+ async checkAutoScale() {
3373
+ if (!this.#auto)
3374
+ return;
3375
+ try {
3376
+ const recommended = await this.fetchRecommendedShardCount();
3377
+ if (recommended !== this.shardCount) {
3378
+ await this.reshard(recommended);
3379
+ }
3380
+ } catch (error) {
3381
+ this.#options.onAutoScaleError?.(error);
3382
+ }
3383
+ }
3384
+ async reshard(count) {
3385
+ this.#options.shardCount = count;
3386
+ this.destroy();
3387
+ await this.connect();
3388
+ }
3389
+ destroy() {
3390
+ for (const shard of this.shards.values())
3391
+ shard.close();
3392
+ for (const id of this.shards.keys())
3393
+ this.#setStatus(id, "destroyed");
3394
+ this.shards.clear();
3395
+ this.#lifecycle.clear();
3396
+ this.#announced = false;
3397
+ this.#resolved = false;
3398
+ this.#destroyed = true;
3399
+ this.#generation++;
3400
+ if (this.#autoScaleTimer) {
3401
+ clearInterval(this.#autoScaleTimer);
3402
+ this.#autoScaleTimer = undefined;
3403
+ }
3404
+ }
3405
+ on(event, listener) {
3406
+ let set = this.#listeners.get(event);
3407
+ if (!set)
3408
+ this.#listeners.set(event, set = new Set);
3409
+ set.add(listener);
3410
+ return this;
3411
+ }
3412
+ off(event, listener) {
3413
+ this.#listeners.get(event)?.delete(listener);
3414
+ return this;
3415
+ }
3416
+ #emit(event, ...args) {
3417
+ for (const listener of this.#listeners.get(event) ?? [])
3418
+ try {
3419
+ listener(...args);
3420
+ } catch {}
3421
+ }
3422
+ getShardIdForGuild(guildId) {
3423
+ const id = BigInt(guildId);
3424
+ return Number((id >> 22n) % BigInt(this.#total));
3425
+ }
3426
+ sendVoiceState(guildId, channelId, options = {}) {
3427
+ return this.shards.get(this.getShardIdForGuild(guildId))?.sendVoiceState(guildId, channelId, options) ?? false;
3428
+ }
3429
+ get(id) {
3430
+ return this.shards.get(id);
3431
+ }
3432
+ health() {
3433
+ return [...this.shards].map(([id, gateway]) => {
3434
+ const life = this.#lifecycle.get(id);
3435
+ return {
3436
+ id,
3437
+ state: gateway.state,
3438
+ ping: gateway.ping,
3439
+ status: life?.status ?? "idle",
3440
+ ...life?.readyAt !== undefined ? { readyAt: life.readyAt } : {},
3441
+ ...life?.disconnectedAt !== undefined ? { disconnectedAt: life.disconnectedAt } : {}
3442
+ };
3443
+ });
3444
+ }
3445
+ report(metrics = false) {
3446
+ let numbers;
3447
+ if (metrics) {
3448
+ const memory = process.memoryUsage();
3449
+ const cpu = process.cpuUsage();
3450
+ numbers = {
3451
+ rssBytes: memory.rss,
3452
+ heapUsedBytes: memory.heapUsed,
3453
+ cpuUserMicros: cpu.user,
3454
+ cpuSystemMicros: cpu.system
3455
+ };
3456
+ }
3457
+ return {
3458
+ shards: this.health(),
3459
+ ...numbers ? { metrics: numbers } : {},
3460
+ reportedAt: Date.now()
3461
+ };
3462
+ }
3463
+ #setStatus(id, status) {
3464
+ const life = this.#lifecycle.get(id) ?? { status };
3465
+ life.status = status;
3466
+ const now = Date.now();
3467
+ if (status === "ready")
3468
+ life.readyAt = now;
3469
+ if (status === "disconnected")
3470
+ life.disconnectedAt = now;
3471
+ this.#lifecycle.set(id, life);
3472
+ if (status !== "ready")
3473
+ this.#announced = false;
3474
+ else
3475
+ this.#maybeAnnounce();
3476
+ }
3477
+ #maybeAnnounce() {
3478
+ if (this.#announced || this.shards.size === 0)
3479
+ return;
3480
+ for (const id of this.shards.keys())
3481
+ if (this.#lifecycle.get(id)?.status !== "ready")
3482
+ return;
3483
+ this.#announced = true;
3484
+ this.#emit("ready");
3485
+ this.#worker?.ready(this.shardIds);
3486
+ this.#worker?.reportHealth(this.report());
3487
+ }
3488
+ values() {
3489
+ return [...this.shards].map(([id, gateway]) => ({ id, gateway }));
3490
+ }
3491
+ async#ensureInitialized() {
3492
+ if (this.#resolved)
3493
+ return;
3494
+ if (this.#destroyed)
3495
+ this.#destroyed = false;
3496
+ const count = this.#options.shardCount === "auto" ? await this.fetchRecommendedShardCount() : this.#options.shardCount ?? this.#fixedCount;
3497
+ this.#initialize(count);
3498
+ }
3499
+ #initialize(count) {
3500
+ if (this.#resolved)
3501
+ return;
3502
+ this.#total = count;
3503
+ const ids = this.#ids ?? Array.from({ length: count }, (_, i) => i);
3504
+ validateShardIds2(ids, count);
3505
+ for (const id of ids)
3506
+ this.shards.set(id, this.#createShard(id, count));
3507
+ this.#resolved = true;
3508
+ }
3509
+ #createShard(id, count) {
3510
+ const gateway = new Gateway2({
3511
+ token: this.#options.token,
3512
+ intents: this.#options.intents,
3513
+ shardId: id,
3514
+ shardCount: count,
3515
+ reconnect: this.#options.reconnect ?? true,
3516
+ ...this.#gate ? {
3517
+ identifyGate: {
3518
+ acquire: () => this.#gate.acquire(id),
3519
+ release: () => this.#gate.release(id)
3520
+ }
3521
+ } : {}
3522
+ });
3523
+ gateway.on("ready", () => {
3524
+ this.#setStatus(id, "ready");
3525
+ this.#emit("shardReady", id);
3526
+ });
3527
+ gateway.on("resumed", () => {
3528
+ this.#setStatus(id, "ready");
3529
+ this.#emit("shardResume", id);
3530
+ });
3531
+ gateway.on("close", (close) => {
3532
+ this.#setStatus(id, "disconnected");
3533
+ this.#emit("shardDisconnect", id, close);
3534
+ });
3535
+ gateway.on("error", (error) => this.#emit("shardError", id, error));
3536
+ gateway.on("unstable", (info) => {
3537
+ this.#setStatus(id, "unstable");
3538
+ this.#emit("shardUnstable", id, info);
3539
+ });
3540
+ this.#emit("shardCreate", id, gateway);
3541
+ return gateway;
3542
+ }
3543
+ }
3544
+
3545
+ export { fetchGatewayBot2, MAX_GUILDS_PER_SHARD2, shardsForGuilds2, fetchGuildCount2, isClusterWorker2, validateShardIds2, allocateShards2, planClusters2, assertCompleteAllocation2, readClusterContext2, threadFork2, threadEndpoint2, ClusterWorker2, BroadcastChannelTransport2, IpcTransport2, ShardBus2, ShardSupervisor2, IDENTIFY_INTERVAL2, IdentifyCoordinator2, REMOTE_PROTOCOL_VERSION2, RemoteClose2, RequestTracker2, ClusterManager2, ClusterHost2, RedisLeaderLock2, SEQUENTIAL_READY_TIMEOUT2, connectInQueue2, ShardManager2 };
3546
+
3547
+ //# debugId=C7DC56012C5DC05564756E2164756E21