@helyx/bot 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1230 @@
1
+ import { safeError, ServiceRegistry } from "@helyx/core";
2
+ import { checkDatabaseConnection, createDatabasePool, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
3
+ import { createPlatformServices, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
4
+ import { Client, ShardingManager } from "discord.js";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { createRequire } from "node:module";
7
+ import { join } from "node:path";
8
+ import pino, {} from "pino";
9
+ import { synchroniseApplicationCommands, } from "./command-sync.js";
10
+ import { CloudConnectorController, } from "./cloud-connector.js";
11
+ import { createCloudManagementModule } from "./cloud-management-module.js";
12
+ import { BotControlService, createBotControlServer, mapBotControlError, } from "./control-server.js";
13
+ import { createManagementModule } from "./management-module.js";
14
+ import { buildApplicationCommandDefinitions } from "./interactions.js";
15
+ import { resolveModulePackageSpecifiers } from "./module-configuration.js";
16
+ import { loadConfiguredModulePackages } from "./modules.js";
17
+ import { createPermissionsModule } from "./permissions-module.js";
18
+ import { SHARD_CONTROL_MAX_BYTES, SHARD_HEALTH_STALE_MS, assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, gatewayBotInformationSchema, shardCloudRequestSchema, shardHealthSnapshotSchema, workerMessageSchema, } from "./shard-contracts.js";
19
+ import { RedisShardCoordinator, } from "./shard-coordinator.js";
20
+ import { ShardControlRouter } from "./shard-router.js";
21
+ import { createDeveloperOperationsModule } from "./developer-operations-module.js";
22
+ import { SupervisorCloudDiscordDirectory } from "./supervisor-cloud-directory.js";
23
+ const LEASE_RENEW_INTERVAL_MS = 10_000;
24
+ const STARTUP_LEASE_RENEW_INTERVAL_MS = 5_000;
25
+ const LEASE_VALIDATION_TIMEOUT_MS = 1_000;
26
+ const RETENTION_CLEANUP_INTERVAL_MS = 60 * 60 * 1_000;
27
+ const STARTUP_WAIT_MS = 60_000;
28
+ const WORKER_SHUTDOWN_MS = 5_000;
29
+ const MAX_RESTARTS_PER_WINDOW = 5;
30
+ const RESTART_WINDOW_MS = 10 * 60 * 1_000;
31
+ export const STEADY_STATE_LEASE_LOSS_BOUND_MS = LEASE_RENEW_INTERVAL_MS + LEASE_VALIDATION_TIMEOUT_MS + WORKER_SHUTDOWN_MS;
32
+ export async function startShardSupervisor(environment, processEnvironment, workerEntry) {
33
+ if (environment.HELYX_SHARD_MODE === "off")
34
+ throw new Error("Shard supervisor requires sharding to be enabled");
35
+ const shardMode = environment.HELYX_SHARD_MODE;
36
+ const logger = pino({
37
+ level: environment.LOG_LEVEL,
38
+ base: { component: "bot-supervisor" },
39
+ });
40
+ let supervisorId = randomUUID();
41
+ const database = createDatabasePool({
42
+ connectionString: environment.DATABASE_URL,
43
+ sslMode: environment.DATABASE_SSL,
44
+ applicationName: "helyx-bot-supervisor",
45
+ ...(environment.HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX === undefined
46
+ ? {}
47
+ : {
48
+ maxConnections: environment.HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX,
49
+ }),
50
+ });
51
+ let privacyDatabase;
52
+ let controlServer;
53
+ let coordinator;
54
+ let leaderLease;
55
+ let cloudConnector;
56
+ let cloudStarted = false;
57
+ let maintainingLeases = false;
58
+ let rebalancingShards = false;
59
+ let retentionTimer;
60
+ let leaseTimer;
61
+ let startupLeaseTimer;
62
+ let startupLeaseRefresh;
63
+ let startupLeaseError;
64
+ let shuttingDown = false;
65
+ const localLeases = new Map();
66
+ const healthByShard = new Map();
67
+ const pendingControl = new Map();
68
+ const restartHistory = new Map();
69
+ const restartCounts = new Map();
70
+ const workers = new Map();
71
+ let sessionBudget;
72
+ let databaseHealthy = false;
73
+ let generationDescriptor;
74
+ let deploymentOrder = 1;
75
+ let shardCount = 1;
76
+ let privacyService;
77
+ let manager;
78
+ let promoteToLeader = () => Promise.resolve(false);
79
+ try {
80
+ await checkDatabaseConnection(database);
81
+ databaseHealthy = true;
82
+ const moduleSpecifiers = await resolveModulePackageSpecifiers(environment.HELYX_MODULE_PACKAGES, undefined, undefined, environment.HELYX_MODULE_CATALOGUE_PATH);
83
+ const modulePackages = await loadConfiguredModulePackages(moduleSpecifiers);
84
+ const featureModules = modulePackages.map(({ definition }) => definition);
85
+ const gateway = await fetchGatewayBotInformation(environment.DISCORD_TOKEN);
86
+ shardCount =
87
+ environment.HELYX_SHARD_MODE === "fixed"
88
+ ? environment.HELYX_SHARD_COUNT
89
+ : gateway.shards;
90
+ if (environment.HELYX_SHARD_SCOPE === "local" &&
91
+ environment.HELYX_SHARD_WORKERS_PER_REPLICA < shardCount)
92
+ throw new Error("Local shard scope requires workers-per-replica to cover every shard");
93
+ const generation = environment.HELYX_SHARD_GENERATION ?? `local-${Date.now().toString(36)}`;
94
+ sessionBudget = {
95
+ total: gateway.session_start_limit.total,
96
+ remaining: gateway.session_start_limit.remaining,
97
+ resetAt: new Date(Date.now() + gateway.session_start_limit.reset_after).toISOString(),
98
+ maxConcurrency: gateway.session_start_limit.max_concurrency,
99
+ observedAt: new Date().toISOString(),
100
+ };
101
+ const supervisorDirectory = new SupervisorCloudDiscordDirectory(environment.DISCORD_TOKEN, database, environment.HELYX_OWNER_USER_IDS);
102
+ cloudConnector = new CloudConnectorController({
103
+ environment,
104
+ database,
105
+ discord: supervisorDirectory,
106
+ modules: modulePackages,
107
+ coreVersion: botVersion(),
108
+ logger,
109
+ });
110
+ const commandClient = new Client({ intents: [] });
111
+ const unavailableResources = {
112
+ getGuildResources: () => Promise.reject(new Error("Guild resources are worker-owned")),
113
+ sendToChannel: () => Promise.reject(new Error("Guild resources are worker-owned")),
114
+ deleteChannelMessage: () => Promise.reject(new Error("Guild resources are worker-owned")),
115
+ };
116
+ const permissionsModule = createPermissionsModule(featureModules);
117
+ const managementModule = createManagementModule(featureModules, unavailableResources);
118
+ const cloudManagementModule = environment.HELYX_CLOUD_CONNECT_ENABLED
119
+ ? createCloudManagementModule({
120
+ connector: cloudConnector,
121
+ client: commandClient,
122
+ discord: supervisorDirectory,
123
+ configuredOwnerUserIds: environment.HELYX_OWNER_USER_IDS,
124
+ })
125
+ : undefined;
126
+ const publicModules = [
127
+ managementModule,
128
+ permissionsModule,
129
+ ...(cloudManagementModule ? [cloudManagementModule] : []),
130
+ ...featureModules,
131
+ ];
132
+ const operationsModule = environment.DISCORD_INTERNAL_GUILD_ID
133
+ ? createDeveloperOperationsModule({
134
+ internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
135
+ approvedRoleIds: environment.APPROVED_ROLES,
136
+ botVersion: botVersion(),
137
+ ...(processEnvironment.RAILWAY_GIT_COMMIT_SHA
138
+ ? {
139
+ deploymentReference: processEnvironment.RAILWAY_GIT_COMMIT_SHA.slice(0, 12),
140
+ }
141
+ : {}),
142
+ coreFunctions: [
143
+ managementModule,
144
+ permissionsModule,
145
+ ...(cloudManagementModule ? [cloudManagementModule] : []),
146
+ ].map(({ manifest }) => manifest),
147
+ modules: featureModules.map(({ manifest }) => manifest),
148
+ processLabel: "shard supervisor registration",
149
+ health: {
150
+ getHealth: async () => boundedOperationalHealth(await buildHealth()),
151
+ },
152
+ })
153
+ : undefined;
154
+ const modules = [
155
+ ...publicModules,
156
+ ...(operationsModule ? [operationsModule] : []),
157
+ ];
158
+ const initialiseLeader = async () => {
159
+ await runLeaderStartup(database, environment, modulePackages, modules, logger);
160
+ assertStartupLeaseHealthy();
161
+ privacyDatabase = await createPrivacyDatabase(environment, database);
162
+ assertStartupLeaseHealthy();
163
+ const services = new Map(createPlatformServices(database, publicModules));
164
+ privacyService = new BotControlService(database, publicModules, unavailableResources, new ServiceRegistry(services), privacyDatabase
165
+ ? new PostgresPrivacyProviderExecutor({
166
+ readPool: database,
167
+ executorPool: privacyDatabase,
168
+ modules: featureModules,
169
+ })
170
+ : undefined);
171
+ cloudConnector?.setControlHandler(new ShardControlRouter({
172
+ generation,
173
+ shardCount,
174
+ localLeases,
175
+ dispatchLocal,
176
+ privacy: privacyService,
177
+ localLeaderLease: leaderLease,
178
+ ...(coordinator ? { coordinator } : {}),
179
+ }));
180
+ await startLeaderSingletons();
181
+ assertStartupLeaseHealthy();
182
+ };
183
+ generationDescriptor = {
184
+ version: 1,
185
+ applicationId: environment.DISCORD_CLIENT_ID,
186
+ environment: environment.NODE_ENV,
187
+ generation,
188
+ shardCount,
189
+ catalogueHash: shardRuntimeCompositionHash(modules, environment.DISCORD_INTERNAL_GUILD_ID),
190
+ };
191
+ deploymentOrder = await nextShardDeploymentOrder(database);
192
+ if (environment.HELYX_SHARD_SCOPE === "coordinated") {
193
+ coordinator = new RedisShardCoordinator(environment.HELYX_SHARD_REDIS_URL, environment.HELYX_SHARD_REDIS_NAMESPACE, {
194
+ onError: (error) => logger.error({ event: "shard.redis_error", error: safeError(error) }, "Shard coordination failed"),
195
+ });
196
+ supervisorId = coordinator.supervisorId;
197
+ await coordinator.start((targetShardId, request) => dispatchLocal(targetShardId, request), (request) => executeLeaderControl(request), (request) => executeLeaderCloud(request));
198
+ deploymentOrder = await coordinator.assertGeneration(generationDescriptor, deploymentOrder);
199
+ leaderLease = await coordinator.acquireLeader(generationDescriptor, deploymentOrder);
200
+ startStartupLeaseMaintenance();
201
+ }
202
+ else {
203
+ leaderLease = {
204
+ generation,
205
+ supervisorId,
206
+ fencingToken: 1,
207
+ };
208
+ }
209
+ promoteToLeader = async () => {
210
+ if (!coordinator || leaderLease || shuttingDown)
211
+ return Boolean(leaderLease);
212
+ const candidate = await acquireFencedLeader(coordinator, generationDescriptor, deploymentOrder).catch(() => undefined);
213
+ if (!candidate)
214
+ return false;
215
+ if (shuttingDown) {
216
+ await coordinator.releaseLeader(candidate).catch(() => false);
217
+ return false;
218
+ }
219
+ leaderLease = candidate;
220
+ startStartupLeaseMaintenance();
221
+ try {
222
+ await initialiseLeader();
223
+ await stopStartupLeaseMaintenance();
224
+ await withTimeout(renewStartupOwnership(coordinator, generationDescriptor, deploymentOrder, leaderLease), LEASE_VALIDATION_TIMEOUT_MS);
225
+ logger.info({
226
+ event: "shard.leader_promoted",
227
+ fencingToken: candidate.fencingToken,
228
+ }, "Shard supervisor acquired the fenced deployment leader lease");
229
+ return true;
230
+ }
231
+ catch (error) {
232
+ logger.error({ event: "shard.leader_promotion_failed", error: safeError(error) }, "Shard supervisor could not promote to deployment leader");
233
+ await shutdown("leader-promotion-failed");
234
+ return false;
235
+ }
236
+ };
237
+ if (leaderLease) {
238
+ await initialiseLeader();
239
+ }
240
+ else {
241
+ await waitForLeaderStartup(database, generation);
242
+ assertStartupLeaseHealthy();
243
+ }
244
+ const router = new ShardControlRouter({
245
+ generation,
246
+ shardCount,
247
+ localLeases,
248
+ dispatchLocal,
249
+ ...(privacyService ? { privacy: privacyService } : {}),
250
+ ...(leaderLease ? { localLeaderLease: leaderLease } : {}),
251
+ ...(coordinator ? { coordinator } : {}),
252
+ });
253
+ if (!leaderLease)
254
+ cloudConnector.setControlHandler(router);
255
+ const claimedIds = await claimInitialShards();
256
+ manager = new ShardingManager(workerEntry, {
257
+ totalShards: shardCount,
258
+ shardList: claimedIds.length > 0 ? claimedIds : [0],
259
+ mode: "process",
260
+ respawn: false,
261
+ token: environment.DISCORD_TOKEN,
262
+ execArgv: process.execArgv,
263
+ });
264
+ await reserveInitialSessionStarts(claimedIds.length, gateway);
265
+ await spawnInIdentifyBuckets(claimedIds, gateway.session_start_limit.max_concurrency);
266
+ assertStartupLeaseHealthy();
267
+ if (environment.HELYX_BOT_CONTROL_SECRET) {
268
+ controlServer = await createBotControlServer({
269
+ service: router,
270
+ secret: environment.HELYX_BOT_CONTROL_SECRET,
271
+ logger: { level: environment.LOG_LEVEL },
272
+ shardHealth: buildHealth,
273
+ });
274
+ await controlServer.listen({
275
+ host: "::",
276
+ port: environment.HELYX_BOT_CONTROL_PORT,
277
+ });
278
+ }
279
+ await stopStartupLeaseMaintenance();
280
+ if (coordinator)
281
+ await withTimeout(renewStartupOwnership(coordinator, generationDescriptor, deploymentOrder, leaderLease), LEASE_VALIDATION_TIMEOUT_MS);
282
+ leaseTimer = setInterval(() => void maintainLeases(), LEASE_RENEW_INTERVAL_MS);
283
+ leaseTimer.unref();
284
+ process.once("SIGINT", () => void shutdown("SIGINT"));
285
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
286
+ logger.info({
287
+ event: "shard.supervisor_ready",
288
+ mode: environment.HELYX_SHARD_MODE,
289
+ scope: environment.HELYX_SHARD_SCOPE,
290
+ generation,
291
+ supervisorId,
292
+ shardCount,
293
+ localShardCount: claimedIds.length,
294
+ }, "Shard supervisor started");
295
+ return { shutdown };
296
+ }
297
+ catch (error) {
298
+ await shutdown("startup-failed");
299
+ throw error;
300
+ }
301
+ async function claimInitialShards() {
302
+ const maximum = environment.HELYX_SHARD_SCOPE === "local"
303
+ ? shardCount
304
+ : environment.HELYX_SHARD_WORKERS_PER_REPLICA;
305
+ const claimed = [];
306
+ for (let shardId = 0; shardId < shardCount && claimed.length < maximum; shardId++) {
307
+ const lease = coordinator && generationDescriptor
308
+ ? await coordinator.claimShard(generationDescriptor, shardId, deploymentOrder)
309
+ : {
310
+ generation: generationDescriptor.generation,
311
+ deploymentOrder,
312
+ supervisorId,
313
+ shardId,
314
+ fencingToken: 1,
315
+ };
316
+ if (!lease)
317
+ continue;
318
+ localLeases.set(shardId, lease);
319
+ claimed.push(shardId);
320
+ }
321
+ return claimed;
322
+ }
323
+ async function reserveInitialSessionStarts(count, gateway) {
324
+ if (count === 0)
325
+ return;
326
+ if (coordinator) {
327
+ const reserved = await coordinator.reserveSessionStarts(count, gateway.session_start_limit.remaining, gateway.session_start_limit.reset_after);
328
+ if (!reserved)
329
+ throw new Error("Discord session-start budget cannot support this replica");
330
+ }
331
+ else if (gateway.session_start_limit.remaining < count) {
332
+ throw new Error("Discord session-start budget cannot support planned shards");
333
+ }
334
+ }
335
+ async function spawnInIdentifyBuckets(shardIds, maxConcurrency) {
336
+ for (let offset = 0; offset < shardIds.length; offset += maxConcurrency) {
337
+ if (shuttingDown)
338
+ return;
339
+ const batch = shardIds.slice(offset, offset + maxConcurrency);
340
+ await Promise.all(batch.map((shardId) => spawnWorker(shardId)));
341
+ if (offset + maxConcurrency < shardIds.length)
342
+ await wait(5_500);
343
+ }
344
+ }
345
+ async function spawnWorker(shardId) {
346
+ if (shuttingDown)
347
+ return;
348
+ const lease = localLeases.get(shardId);
349
+ if (!lease || !manager)
350
+ throw new Error("Shard lease is unavailable");
351
+ if (coordinator) {
352
+ const allowed = await coordinator.acquireIdentifyPermit(shardId, sessionBudget?.maxConcurrency ?? 1, 15_000);
353
+ if (!allowed)
354
+ throw new Error("Discord identify bucket is unavailable");
355
+ }
356
+ if (shuttingDown)
357
+ return;
358
+ let shard = workers.get(shardId);
359
+ if (!shard) {
360
+ shard = manager.createShard(shardId);
361
+ workers.set(shardId, shard);
362
+ shard.on("message", (raw) => void handleWorkerMessage(shardId, raw));
363
+ shard.on("death", () => handleWorkerDeath(shardId));
364
+ shard.on("error", (error) => logger.error({ event: "shard.worker_error", shardId, error: safeError(error) }, "Shard worker emitted an error"));
365
+ }
366
+ const identity = {
367
+ version: 1,
368
+ mode: shardMode,
369
+ scope: environment.HELYX_SHARD_SCOPE,
370
+ generation: generationDescriptor.generation,
371
+ deploymentOrder: lease.deploymentOrder,
372
+ supervisorId: lease.supervisorId,
373
+ shardId,
374
+ shardCount,
375
+ fencingToken: lease.fencingToken,
376
+ };
377
+ shard.env = buildShardWorkerEnvironment(processEnvironment, identity, restartCounts.get(shardId) ?? 0);
378
+ healthByShard.set(shardId, startingHealth(shardId, restartCounts.get(shardId) ?? 0));
379
+ const launchHeartbeatTimer = coordinator
380
+ ? setInterval(() => void sendLeaseHeartbeat(shardId, lease), STARTUP_LEASE_RENEW_INTERVAL_MS)
381
+ : undefined;
382
+ launchHeartbeatTimer?.unref();
383
+ try {
384
+ await spawnWithShutdownGuard(() => shuttingDown, () => shard.spawn(30_000), () => shard.kill());
385
+ }
386
+ finally {
387
+ if (launchHeartbeatTimer)
388
+ clearInterval(launchHeartbeatTimer);
389
+ }
390
+ await sendLeaseHeartbeat(shardId, lease);
391
+ }
392
+ async function sendLeaseHeartbeat(shardId, lease) {
393
+ if (shuttingDown || !coordinator || !workers.get(shardId)?.process)
394
+ return;
395
+ const identity = {
396
+ version: 1,
397
+ mode: shardMode,
398
+ scope: environment.HELYX_SHARD_SCOPE,
399
+ generation: lease.generation,
400
+ deploymentOrder: lease.deploymentOrder,
401
+ supervisorId: lease.supervisorId,
402
+ shardId,
403
+ shardCount,
404
+ fencingToken: lease.fencingToken,
405
+ };
406
+ const heartbeat = {
407
+ version: 1,
408
+ type: "helyx.lease.heartbeat",
409
+ identity,
410
+ };
411
+ assertShardMessageSize(heartbeat);
412
+ await workers
413
+ .get(shardId)
414
+ ?.send(heartbeat)
415
+ .catch(() => undefined);
416
+ }
417
+ async function handleWorkerMessage(shardId, raw) {
418
+ if (serializedBytes(raw) > SHARD_CONTROL_MAX_BYTES)
419
+ return;
420
+ const message = workerMessageSchema.safeParse(raw);
421
+ if (!message.success)
422
+ return;
423
+ if (message.data.type === "helyx.health.update") {
424
+ const lease = localLeases.get(shardId);
425
+ if (lease &&
426
+ message.data.identity.shardId === shardId &&
427
+ message.data.identity.generation === lease.generation &&
428
+ message.data.identity.fencingToken === lease.fencingToken) {
429
+ healthByShard.set(shardId, message.data.health);
430
+ await coordinator
431
+ ?.setHealth(message.data.identity, message.data.health)
432
+ .catch(() => undefined);
433
+ }
434
+ return;
435
+ }
436
+ if (message.data.type === "helyx.control.response") {
437
+ const pending = pendingControl.get(message.data.requestId);
438
+ if (!pending || pending.shardId !== shardId)
439
+ return;
440
+ pendingControl.delete(message.data.requestId);
441
+ clearTimeout(pending.timer);
442
+ pending.resolve(message.data);
443
+ return;
444
+ }
445
+ if (message.data.type === "helyx.cloud.request") {
446
+ const response = leaderLease
447
+ ? await executeLeaderCloud(message.data)
448
+ : await routeCloudToLeader(message.data);
449
+ await workers.get(shardId)?.send(response);
450
+ return;
451
+ }
452
+ if (message.data.type === "helyx.operations.health.request") {
453
+ const response = {
454
+ version: 1,
455
+ type: "helyx.operations.health.response",
456
+ requestId: message.data.requestId,
457
+ health: boundedOperationalHealth(await buildHealth()),
458
+ };
459
+ assertShardMessageSize(response);
460
+ await workers.get(shardId)?.send(response);
461
+ }
462
+ }
463
+ function dispatchLocal(shardId, request) {
464
+ const shard = workers.get(shardId);
465
+ if (!shard?.process)
466
+ return Promise.resolve(unavailableControl(request.requestId));
467
+ return new Promise((resolve) => {
468
+ const timer = setTimeout(() => {
469
+ pendingControl.delete(request.requestId);
470
+ resolve(unavailableControl(request.requestId, 504));
471
+ }, Math.max(1, new Date(request.deadlineAt).getTime() - Date.now()));
472
+ timer.unref();
473
+ pendingControl.set(request.requestId, { shardId, resolve, timer });
474
+ void shard.send(request).catch(() => {
475
+ clearTimeout(timer);
476
+ pendingControl.delete(request.requestId);
477
+ resolve(unavailableControl(request.requestId));
478
+ });
479
+ });
480
+ }
481
+ async function executeLeaderControl(request) {
482
+ if (!leaderLease || !privacyService)
483
+ return unavailableControl(request.requestId);
484
+ if (request.generation !== leaderLease.generation ||
485
+ request.fencingToken !== leaderLease.fencingToken ||
486
+ controlOperationGuildId(request.operation))
487
+ return unavailableControl(request.requestId);
488
+ try {
489
+ return {
490
+ version: 1,
491
+ type: "helyx.control.response",
492
+ requestId: request.requestId,
493
+ ok: true,
494
+ result: await executeShardControlOperation(privacyService, request.operation),
495
+ };
496
+ }
497
+ catch (error) {
498
+ const mapped = mapBotControlError(error);
499
+ return {
500
+ version: 1,
501
+ type: "helyx.control.response",
502
+ requestId: request.requestId,
503
+ ok: false,
504
+ status: mapped?.status ?? 500,
505
+ message: mapped?.message ?? "The shard leader rejected the request",
506
+ };
507
+ }
508
+ }
509
+ async function executeLeaderCloud(request) {
510
+ const parsed = shardCloudRequestSchema.parse(request);
511
+ if (!leaderLease ||
512
+ !(await currentLeaderOwnership()) ||
513
+ !cloudConnector ||
514
+ !cloudStarted ||
515
+ cloudRequestExpired(parsed))
516
+ return failedCloud(parsed.requestId, "Cloud leader is unavailable");
517
+ try {
518
+ let result;
519
+ switch (parsed.operation.kind) {
520
+ case "cloud.status":
521
+ result = cloudConnector.status();
522
+ break;
523
+ case "cloud.consent.get":
524
+ result = await cloudConnector.getSupportConsent();
525
+ break;
526
+ case "cloud.consent.update": {
527
+ if (cloudRequestExpired(parsed))
528
+ return failedCloud(parsed.requestId, "Cloud request expired");
529
+ result = await cloudConnector.updateSupportConsent({
530
+ supportInspectionEnabled: parsed.operation.supportInspectionEnabled,
531
+ supportDataClasses: parsed.operation.supportDataClasses,
532
+ diagnosticsEnabled: parsed.operation.diagnosticsEnabled,
533
+ });
534
+ break;
535
+ }
536
+ case "cloud.enrol":
537
+ if (cloudRequestExpired(parsed))
538
+ return failedCloud(parsed.requestId, "Cloud request expired");
539
+ result = await cloudConnector.enrol(parsed.operation.token, parsed.operation.source);
540
+ break;
541
+ case "cloud.disconnect":
542
+ if (cloudRequestExpired(parsed))
543
+ return failedCloud(parsed.requestId, "Cloud request expired");
544
+ await cloudConnector.disconnect();
545
+ result = null;
546
+ break;
547
+ case "cloud.rotate":
548
+ if (cloudRequestExpired(parsed))
549
+ return failedCloud(parsed.requestId, "Cloud request expired");
550
+ result = await cloudConnector.rotate();
551
+ break;
552
+ }
553
+ const response = {
554
+ version: 1,
555
+ type: "helyx.cloud.response",
556
+ requestId: parsed.requestId,
557
+ ok: true,
558
+ result,
559
+ };
560
+ assertShardMessageSize(response);
561
+ return response;
562
+ }
563
+ catch {
564
+ return failedCloud(parsed.requestId, "Cloud operation failed safely");
565
+ }
566
+ }
567
+ async function routeCloudToLeader(request) {
568
+ const lease = await coordinator?.getLeaderLease().catch(() => undefined);
569
+ if (!coordinator || !lease)
570
+ return failedCloud(request.requestId, "Cloud leader is unavailable");
571
+ return coordinator
572
+ .routeLeaderCloud(lease, request)
573
+ .catch(() => failedCloud(request.requestId, "Cloud leader is unavailable"));
574
+ }
575
+ function handleWorkerDeath(shardId) {
576
+ healthByShard.set(shardId, {
577
+ ...startingHealth(shardId, restartCounts.get(shardId) ?? 0),
578
+ status: "failed",
579
+ });
580
+ for (const [requestId, pending] of pendingControl) {
581
+ if (pending.shardId !== shardId)
582
+ continue;
583
+ pendingControl.delete(requestId);
584
+ clearTimeout(pending.timer);
585
+ pending.resolve(unavailableControl(requestId));
586
+ }
587
+ if (shuttingDown || !localLeases.has(shardId))
588
+ return;
589
+ const now = Date.now();
590
+ const recent = (restartHistory.get(shardId) ?? []).filter((time) => now - time < RESTART_WINDOW_MS);
591
+ if (recent.length >= MAX_RESTARTS_PER_WINDOW) {
592
+ logger.error({ event: "shard.circuit_open", shardId, restartCount: recent.length }, "Shard worker restart circuit opened");
593
+ return;
594
+ }
595
+ recent.push(now);
596
+ restartHistory.set(shardId, recent);
597
+ restartCounts.set(shardId, (restartCounts.get(shardId) ?? 0) + 1);
598
+ const delay = boundedRespawnDelay(recent.length);
599
+ setTimeout(() => void respawnWorker(shardId), delay).unref();
600
+ }
601
+ async function respawnWorker(shardId) {
602
+ if (shuttingDown || !localLeases.has(shardId))
603
+ return;
604
+ try {
605
+ const gateway = await fetchGatewayBotInformation(environment.DISCORD_TOKEN);
606
+ await reserveInitialSessionStarts(1, gateway);
607
+ await spawnWorker(shardId);
608
+ }
609
+ catch (error) {
610
+ logger.error({ event: "shard.respawn_failed", shardId, error: safeError(error) }, "Shard worker respawn failed");
611
+ handleWorkerDeath(shardId);
612
+ }
613
+ }
614
+ async function maintainLeases() {
615
+ if (shuttingDown || maintainingLeases)
616
+ return;
617
+ maintainingLeases = true;
618
+ try {
619
+ if (!coordinator)
620
+ return;
621
+ const renewal = await renewSteadyStateOwnership(coordinator, generationDescriptor, deploymentOrder, localLeases, leaderLease).catch(() => undefined);
622
+ if (!renewal?.generationRenewed) {
623
+ logger.error({ event: "shard.generation_lease_lost" }, "Shard generation lease was lost; stopping this supervisor safely");
624
+ await shutdown("generation-lease-lost");
625
+ return;
626
+ }
627
+ for (const shardId of renewal.lostShardIds) {
628
+ localLeases.delete(shardId);
629
+ workers.get(shardId)?.kill();
630
+ healthByShard.set(shardId, {
631
+ ...startingHealth(shardId, restartCounts.get(shardId) ?? 0),
632
+ status: "failed",
633
+ });
634
+ }
635
+ if (leaderLease) {
636
+ if (!renewal.leaderRenewed) {
637
+ logger.error({ event: "shard.leader_lease_lost" }, "Shard leader lease was lost; stopping this supervisor safely");
638
+ await shutdown("leader-lease-lost");
639
+ return;
640
+ }
641
+ }
642
+ else {
643
+ if (!renewal.leaderAvailable) {
644
+ const promoted = await promoteToLeader();
645
+ if (!promoted && shuttingDown)
646
+ return;
647
+ }
648
+ }
649
+ await Promise.all([...localLeases].map(([shardId, lease]) => sendLeaseHeartbeat(shardId, lease)));
650
+ }
651
+ finally {
652
+ maintainingLeases = false;
653
+ }
654
+ if (!shuttingDown)
655
+ await rebalanceShards().catch(async (error) => {
656
+ logger.error({ event: "shard.rebalance_failed", error: safeError(error) }, "Shard rebalance failed; stopping this supervisor safely");
657
+ await shutdown("rebalance-failed");
658
+ });
659
+ }
660
+ async function rebalanceShards() {
661
+ if (!coordinator || rebalancingShards)
662
+ return;
663
+ const capacity = environment.HELYX_SHARD_WORKERS_PER_REPLICA - localLeases.size;
664
+ if (capacity < 1)
665
+ return;
666
+ rebalancingShards = true;
667
+ try {
668
+ const occupied = new Set((await withTimeout(coordinator.getShardLeases(shardCount), LEASE_VALIDATION_TIMEOUT_MS).catch(() => [])).map(({ shardId }) => shardId));
669
+ const candidates = Array.from({ length: shardCount }, (_, shardId) => shardId)
670
+ .filter((shardId) => !localLeases.has(shardId) && !occupied.has(shardId))
671
+ .slice(0, capacity);
672
+ const results = await Promise.all(candidates.map(async (shardId) => {
673
+ const lease = await withTimeout(coordinator.claimShard(generationDescriptor, shardId, deploymentOrder), LEASE_VALIDATION_TIMEOUT_MS).catch(() => undefined);
674
+ return lease ? { shardId, lease } : undefined;
675
+ }));
676
+ const claimed = [];
677
+ for (const result of results) {
678
+ if (!result || shuttingDown)
679
+ continue;
680
+ localLeases.set(result.shardId, result.lease);
681
+ claimed.push(result.shardId);
682
+ }
683
+ if (claimed.length < 1 || shuttingDown)
684
+ return;
685
+ const gateway = await fetchGatewayBotInformation(environment.DISCORD_TOKEN);
686
+ if (shuttingDown)
687
+ return;
688
+ await reserveInitialSessionStarts(claimed.length, gateway);
689
+ if (shuttingDown)
690
+ return;
691
+ await spawnInIdentifyBuckets(claimed, gateway.session_start_limit.max_concurrency);
692
+ }
693
+ finally {
694
+ rebalancingShards = false;
695
+ }
696
+ }
697
+ async function startLeaderSingletons() {
698
+ await cleanupExpiredCoreRecords(database, logger);
699
+ retentionTimer = setInterval(() => void cleanupExpiredCoreRecords(database, logger), RETENTION_CLEANUP_INTERVAL_MS);
700
+ retentionTimer.unref();
701
+ await cloudConnector?.start();
702
+ cloudStarted = true;
703
+ }
704
+ function startStartupLeaseMaintenance() {
705
+ if (!coordinator || startupLeaseTimer)
706
+ return;
707
+ const refresh = () => {
708
+ if (startupLeaseRefresh || startupLeaseError)
709
+ return;
710
+ startupLeaseRefresh = withTimeout(renewStartupOwnership(coordinator, generationDescriptor, deploymentOrder, leaderLease), LEASE_VALIDATION_TIMEOUT_MS)
711
+ .catch((error) => {
712
+ startupLeaseError =
713
+ error instanceof Error
714
+ ? error
715
+ : new Error("Shard startup ownership was lost");
716
+ })
717
+ .finally(() => {
718
+ startupLeaseRefresh = undefined;
719
+ });
720
+ };
721
+ startupLeaseTimer = setInterval(refresh, STARTUP_LEASE_RENEW_INTERVAL_MS);
722
+ startupLeaseTimer.unref();
723
+ }
724
+ function assertStartupLeaseHealthy() {
725
+ if (startupLeaseError)
726
+ throw startupLeaseError;
727
+ }
728
+ async function stopStartupLeaseMaintenance() {
729
+ if (startupLeaseTimer)
730
+ clearInterval(startupLeaseTimer);
731
+ startupLeaseTimer = undefined;
732
+ await startupLeaseRefresh;
733
+ assertStartupLeaseHealthy();
734
+ }
735
+ async function currentLeaderOwnership() {
736
+ if (!leaderLease)
737
+ return false;
738
+ if (!coordinator)
739
+ return true;
740
+ try {
741
+ const current = await withTimeout(coordinator.getLeaderLease(), LEASE_VALIDATION_TIMEOUT_MS);
742
+ return (current?.generation === leaderLease.generation &&
743
+ current.supervisorId === leaderLease.supervisorId &&
744
+ current.fencingToken === leaderLease.fencingToken);
745
+ }
746
+ catch {
747
+ return false;
748
+ }
749
+ }
750
+ async function buildHealth() {
751
+ const now = Date.now();
752
+ let redisReady = true;
753
+ if (coordinator)
754
+ await withTimeout(coordinator.checkReady(), LEASE_VALIDATION_TIMEOUT_MS).catch(() => {
755
+ redisReady = false;
756
+ });
757
+ await checkDatabaseConnection(database)
758
+ .then(() => {
759
+ databaseHealthy = true;
760
+ })
761
+ .catch(() => {
762
+ databaseHealthy = false;
763
+ });
764
+ let coordinatedHealth = [];
765
+ let liveLeases = [];
766
+ if (coordinator) {
767
+ try {
768
+ [coordinatedHealth, liveLeases] = await withTimeout(Promise.all([
769
+ coordinator.getHealth(shardCount),
770
+ coordinator.getShardLeases(shardCount),
771
+ ]), LEASE_VALIDATION_TIMEOUT_MS);
772
+ }
773
+ catch {
774
+ redisReady = false;
775
+ }
776
+ }
777
+ const coordinationRedis = coordinator ? (redisReady ? "healthy" : "unavailable") : "not_required";
778
+ const healthMap = selectFencedShardHealth({
779
+ generation: generationDescriptor.generation,
780
+ shardCount,
781
+ coordinatedHealth,
782
+ liveLeases,
783
+ localHealth: healthByShard,
784
+ localLeases,
785
+ coordinated: Boolean(coordinator),
786
+ });
787
+ const shards = Array.from({ length: shardCount }, (_, shardId) => {
788
+ const value = healthMap.get(shardId);
789
+ if (!value)
790
+ return { ...startingHealth(shardId, 0), status: "failed" };
791
+ if (now - new Date(value.observedAt).getTime() > SHARD_HEALTH_STALE_MS)
792
+ return { ...value, status: "failed" };
793
+ return value;
794
+ });
795
+ const readyShardCount = shards.filter((shard) => shard.status === "ready").length;
796
+ const stale = [...healthMap.values()].some((value) => now - new Date(value.observedAt).getTime() > SHARD_HEALTH_STALE_MS);
797
+ return shardHealthSnapshotSchema.parse({
798
+ version: 1,
799
+ mode: shardMode,
800
+ scope: environment.HELYX_SHARD_SCOPE,
801
+ status: stale
802
+ ? "stale"
803
+ : readyShardCount === shardCount &&
804
+ coordinationRedis !== "unavailable" &&
805
+ databaseHealthy
806
+ ? "healthy"
807
+ : readyShardCount === 0
808
+ ? "unavailable"
809
+ : readyShardCount < shardCount
810
+ ? "starting"
811
+ : "degraded",
812
+ observedAt: new Date().toISOString(),
813
+ generation: generationDescriptor.generation,
814
+ supervisorId,
815
+ configuredShardCount: shardCount,
816
+ readyShardCount,
817
+ coordination: {
818
+ redis: coordinationRedis,
819
+ database: databaseHealthy ? "healthy" : "unavailable",
820
+ },
821
+ ...(sessionBudget ? { sessionStarts: sessionBudget } : {}),
822
+ shards,
823
+ });
824
+ }
825
+ async function shutdown(reason = "programmatic") {
826
+ if (shuttingDown)
827
+ return;
828
+ shuttingDown = true;
829
+ if (leaseTimer)
830
+ clearInterval(leaseTimer);
831
+ if (startupLeaseTimer)
832
+ clearInterval(startupLeaseTimer);
833
+ if (retentionTimer)
834
+ clearInterval(retentionTimer);
835
+ await controlServer?.close().catch(() => undefined);
836
+ await Promise.allSettled([...workers.values()].map(async (shard) => {
837
+ if (!shard.process)
838
+ return;
839
+ await shard
840
+ .send({ version: 1, type: "helyx.shutdown", reason })
841
+ .catch(() => undefined);
842
+ await waitForDeath(shard, WORKER_SHUTDOWN_MS);
843
+ if (shard.process)
844
+ shard.kill();
845
+ }));
846
+ await cloudConnector?.stop().catch(() => undefined);
847
+ if (coordinator) {
848
+ const activeCoordinator = coordinator;
849
+ await Promise.allSettled([...localLeases.values()].map((lease) => activeCoordinator.releaseShard(lease)));
850
+ if (leaderLease)
851
+ await activeCoordinator.releaseLeader(leaderLease).catch(() => false);
852
+ await activeCoordinator.stop();
853
+ }
854
+ await privacyDatabase?.end().catch(() => undefined);
855
+ await database.end().catch(() => undefined);
856
+ logger.info({ event: "shard.supervisor_stopped", reason }, "Shard supervisor stopped");
857
+ }
858
+ }
859
+ async function runLeaderStartup(database, environment, modulePackages, modules, logger) {
860
+ await runCoreMigrations(database);
861
+ await runConfiguredModuleMigrations(database, modulePackages.map(({ definition, packageRoot }) => ({
862
+ moduleId: definition.manifest.id,
863
+ moduleVersion: definition.manifest.version,
864
+ migrationsDirectory: join(packageRoot, "migrations"),
865
+ })));
866
+ const repository = new RuntimeStateRepository(database);
867
+ await synchroniseApplicationCommands({
868
+ applicationId: environment.DISCORD_CLIENT_ID,
869
+ token: environment.DISCORD_TOKEN,
870
+ modules,
871
+ ...(environment.DISCORD_INTERNAL_GUILD_ID
872
+ ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
873
+ : {}),
874
+ state: new RuntimeCommandHashStore(repository),
875
+ });
876
+ await setRuntimeState(repository, `shards:${environment.HELYX_SHARD_GENERATION ?? "local"}:startup`, {
877
+ ready: true,
878
+ completedAt: new Date().toISOString(),
879
+ });
880
+ logger.info({ event: "shard.singleton_startup_complete" }, "Shard singleton startup complete");
881
+ }
882
+ class RuntimeCommandHashStore {
883
+ repository;
884
+ constructor(repository) {
885
+ this.repository = repository;
886
+ }
887
+ get(key) {
888
+ return this.repository.get(key);
889
+ }
890
+ async set(key, value, expectedVersion = null) {
891
+ const updated = await this.repository.compareAndSet({
892
+ key,
893
+ value: value,
894
+ expectedVersion,
895
+ });
896
+ if (!updated)
897
+ throw new Error("Runtime state changed concurrently");
898
+ return updated;
899
+ }
900
+ }
901
+ async function setRuntimeState(repository, key, value) {
902
+ const current = await repository.get(key);
903
+ const updated = await repository.compareAndSet({
904
+ key,
905
+ value,
906
+ expectedVersion: current?.version ?? null,
907
+ });
908
+ if (!updated)
909
+ throw new Error("Runtime state changed concurrently");
910
+ }
911
+ async function waitForLeaderStartup(database, generation) {
912
+ const deadline = Date.now() + STARTUP_WAIT_MS;
913
+ while (Date.now() < deadline) {
914
+ try {
915
+ const state = await new RuntimeStateRepository(database).get(`shards:${generation}:startup`);
916
+ if (typeof state?.value === "object" &&
917
+ state.value !== null &&
918
+ !Array.isArray(state.value) &&
919
+ state.value.ready === true)
920
+ return;
921
+ }
922
+ catch {
923
+ // The deployment leader may still be applying the additive migration.
924
+ }
925
+ await wait(250);
926
+ }
927
+ throw new Error("Timed out waiting for shard deployment leader startup");
928
+ }
929
+ async function createPrivacyDatabase(environment, runtimeDatabase) {
930
+ if (!environment.HELYX_PRIVACY_ENABLED ||
931
+ !environment.HELYX_PRIVACY_EXECUTOR_DATABASE_URL ||
932
+ !environment.HELYX_PRIVACY_EXECUTOR_DATABASE_SSL)
933
+ return undefined;
934
+ const privacy = createDatabasePool({
935
+ connectionString: environment.HELYX_PRIVACY_EXECUTOR_DATABASE_URL,
936
+ sslMode: environment.HELYX_PRIVACY_EXECUTOR_DATABASE_SSL,
937
+ applicationName: "helyx-bot-shard-privacy-leader",
938
+ maxConnections: 2,
939
+ });
940
+ await checkDatabaseConnection(privacy);
941
+ const [runtimeIdentity, privacyIdentity] = await Promise.all([
942
+ runtimeDatabase.query("SELECT current_user::text AS current_user"),
943
+ privacy.query("SELECT current_user::text AS current_user"),
944
+ ]);
945
+ if (runtimeIdentity.rows[0]?.current_user ===
946
+ privacyIdentity.rows[0]?.current_user) {
947
+ await privacy.end();
948
+ throw new Error("Privacy executor must use a distinct least-privilege database role");
949
+ }
950
+ return privacy;
951
+ }
952
+ async function fetchGatewayBotInformation(token) {
953
+ const response = await fetch("https://discord.com/api/v10/gateway/bot", {
954
+ headers: { Authorization: `Bot ${token}` },
955
+ signal: AbortSignal.timeout(10_000),
956
+ });
957
+ if (!response.ok)
958
+ throw new Error("Discord gateway planning request failed");
959
+ return gatewayBotInformationSchema.parse(await response.json());
960
+ }
961
+ export function buildShardWorkerEnvironment(source, identity, restartCount) {
962
+ const allowed = new Set([
963
+ "DATABASE_URL",
964
+ "DATABASE_SSL",
965
+ "DISCORD_TOKEN",
966
+ "DISCORD_CLIENT_ID",
967
+ "DISCORD_INTERNAL_GUILD_ID",
968
+ "HELYX_MODULE_PACKAGES",
969
+ "HELYX_MODULE_CATALOGUE_PATH",
970
+ "HELYX_CLOUD_CONNECT_ENABLED",
971
+ "HELYX_OWNER_USER_IDS",
972
+ "APPROVED_ROLES",
973
+ "RAILWAY_GIT_COMMIT_SHA",
974
+ "HELYX_SHARD_MODE",
975
+ "HELYX_SHARD_COUNT",
976
+ "HELYX_SHARD_SCOPE",
977
+ "HELYX_SHARD_WORKERS_PER_REPLICA",
978
+ "HELYX_SHARD_REDIS_NAMESPACE",
979
+ "HELYX_SHARD_GENERATION",
980
+ "HELYX_SHARD_WORKER_DATABASE_POOL_MAX",
981
+ "HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX",
982
+ "HELYX_SHARD_DATABASE_CONNECTION_BUDGET",
983
+ "LOG_LEVEL",
984
+ "NODE_ENV",
985
+ "PATH",
986
+ "SystemRoot",
987
+ "TEMP",
988
+ "TMP",
989
+ ]);
990
+ const environment = Object.fromEntries(Object.entries(source).filter(([key, value]) => allowed.has(key) && value !== undefined));
991
+ return {
992
+ ...environment,
993
+ SHARDING_MANAGER: "true",
994
+ SHARDING_MANAGER_MODE: "process",
995
+ SHARDS: String(identity.shardId),
996
+ SHARD_COUNT: String(identity.shardCount),
997
+ HELYX_SHARD_WORKER_CONTRACT: JSON.stringify(identity),
998
+ HELYX_SHARD_RESTART_COUNT: String(restartCount),
999
+ };
1000
+ }
1001
+ export function shardRuntimeCompositionHash(modules, internalGuildId) {
1002
+ const commands = buildApplicationCommandDefinitions(modules, internalGuildId);
1003
+ return createHash("sha256")
1004
+ .update(JSON.stringify({
1005
+ botVersion: botVersion(),
1006
+ modules: modules
1007
+ .map(({ manifest }) => ({
1008
+ id: manifest.id,
1009
+ version: manifest.version,
1010
+ }))
1011
+ .sort((left, right) => left.id.localeCompare(right.id)),
1012
+ commands: {
1013
+ global: commands.global.sort((left, right) => left.name.localeCompare(right.name)),
1014
+ guild: commands.guild.sort((left, right) => left.name.localeCompare(right.name)),
1015
+ },
1016
+ }))
1017
+ .digest("hex");
1018
+ }
1019
+ function startingHealth(shardId, restartCount) {
1020
+ return {
1021
+ shardId,
1022
+ status: "starting",
1023
+ guildCount: 0,
1024
+ gatewayPingMs: -1,
1025
+ uptimeSeconds: 0,
1026
+ restartCount,
1027
+ eventLoopDelayMs: 0,
1028
+ memoryRssBytes: 0,
1029
+ queueDepth: 0,
1030
+ observedAt: new Date().toISOString(),
1031
+ };
1032
+ }
1033
+ function unavailableControl(requestId, status = 503) {
1034
+ return {
1035
+ version: 1,
1036
+ type: "helyx.control.response",
1037
+ requestId,
1038
+ ok: false,
1039
+ status,
1040
+ message: status === 504
1041
+ ? "The owning shard did not respond in time"
1042
+ : "The owning shard is temporarily unavailable",
1043
+ };
1044
+ }
1045
+ function failedCloud(requestId, message) {
1046
+ return {
1047
+ version: 1,
1048
+ type: "helyx.cloud.response",
1049
+ requestId,
1050
+ ok: false,
1051
+ message,
1052
+ };
1053
+ }
1054
+ async function cleanupExpiredCoreRecords(database, logger) {
1055
+ try {
1056
+ const [sessions, rateLimits] = await Promise.all([
1057
+ new InteractionSessionRepository(database).deleteExpired(),
1058
+ new RateLimitRepository(database).deleteExpired(),
1059
+ ]);
1060
+ if (sessions > 0 || rateLimits > 0)
1061
+ logger.info({
1062
+ event: "database.retention_cleaned",
1063
+ interactionSessions: sessions,
1064
+ rateLimitWindows: rateLimits,
1065
+ }, "Expired core records removed");
1066
+ }
1067
+ catch (error) {
1068
+ logger.warn({ event: "database.retention_cleanup_failed", error: safeError(error) }, "Expired core record cleanup failed");
1069
+ }
1070
+ }
1071
+ function serializedBytes(value) {
1072
+ try {
1073
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
1074
+ }
1075
+ catch {
1076
+ return SHARD_CONTROL_MAX_BYTES + 1;
1077
+ }
1078
+ }
1079
+ function boundedOperationalHealth(health) {
1080
+ return shardHealthSnapshotSchema.parse({
1081
+ ...health,
1082
+ shards: health.shards.slice(0, 25),
1083
+ });
1084
+ }
1085
+ function wait(milliseconds) {
1086
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
1087
+ }
1088
+ async function withTimeout(operation, timeoutMs) {
1089
+ let timer;
1090
+ try {
1091
+ return await Promise.race([
1092
+ operation,
1093
+ new Promise((_resolve, reject) => {
1094
+ timer = setTimeout(() => reject(new Error("Shard ownership validation timed out")), timeoutMs);
1095
+ timer.unref();
1096
+ }),
1097
+ ]);
1098
+ }
1099
+ finally {
1100
+ if (timer)
1101
+ clearTimeout(timer);
1102
+ }
1103
+ }
1104
+ async function waitForDeath(shard, timeoutMs) {
1105
+ if (!shard.process)
1106
+ return;
1107
+ await Promise.race([
1108
+ new Promise((resolve) => shard.once("death", () => resolve())),
1109
+ wait(timeoutMs),
1110
+ ]);
1111
+ }
1112
+ export function boundedRespawnDelay(attempt) {
1113
+ return Math.min(30_000, 1_000 * 2 ** Math.max(0, attempt - 1));
1114
+ }
1115
+ export function selectFencedShardHealth(input) {
1116
+ const liveByShard = new Map(input.liveLeases.map((lease) => [lease.shardId, lease]));
1117
+ const selected = new Map();
1118
+ for (const record of input.coordinatedHealth) {
1119
+ const lease = liveByShard.get(record.identity.shardId);
1120
+ if (record.identity.generation !== input.generation ||
1121
+ record.identity.shardCount !== input.shardCount ||
1122
+ record.health.shardId !== record.identity.shardId ||
1123
+ !lease ||
1124
+ !identityMatchesLease(record.identity, lease))
1125
+ continue;
1126
+ selected.set(record.identity.shardId, record.health);
1127
+ }
1128
+ for (const [shardId, health] of input.localHealth) {
1129
+ if (!input.coordinated) {
1130
+ selected.set(shardId, health);
1131
+ continue;
1132
+ }
1133
+ const localLease = input.localLeases.get(shardId);
1134
+ const liveLease = liveByShard.get(shardId);
1135
+ if (localLease && liveLease && sameShardLease(localLease, liveLease))
1136
+ selected.set(shardId, health);
1137
+ }
1138
+ return selected;
1139
+ }
1140
+ export async function renewSteadyStateOwnership(coordinator, descriptor, deploymentOrder, shardLeases, leaderLease, timeoutMs = LEASE_VALIDATION_TIMEOUT_MS) {
1141
+ return withTimeout((async () => {
1142
+ const generationRenewed = await coordinator.refreshGeneration(descriptor, deploymentOrder);
1143
+ if (!generationRenewed)
1144
+ return {
1145
+ generationRenewed: false,
1146
+ lostShardIds: [...shardLeases.keys()],
1147
+ leaderRenewed: false,
1148
+ leaderAvailable: false,
1149
+ };
1150
+ const shardRenewals = await Promise.all([...shardLeases].map(async ([shardId, lease]) => ({
1151
+ shardId,
1152
+ renewed: await coordinator.renewShard(lease),
1153
+ })));
1154
+ const currentLeader = leaderLease
1155
+ ? undefined
1156
+ : await coordinator.getLeaderLease();
1157
+ const leaderRenewed = leaderLease
1158
+ ? await coordinator.renewLeader(leaderLease)
1159
+ : false;
1160
+ return {
1161
+ generationRenewed: true,
1162
+ lostShardIds: shardRenewals
1163
+ .filter(({ renewed }) => !renewed)
1164
+ .map(({ shardId }) => shardId),
1165
+ leaderRenewed,
1166
+ leaderAvailable: leaderLease ? leaderRenewed : Boolean(currentLeader),
1167
+ };
1168
+ })(), timeoutMs);
1169
+ }
1170
+ export async function acquireFencedLeader(coordinator, descriptor, deploymentOrder, timeoutMs = LEASE_VALIDATION_TIMEOUT_MS) {
1171
+ return withTimeout(coordinator.acquireLeader(descriptor, deploymentOrder), timeoutMs);
1172
+ }
1173
+ export function cloudRequestExpired(request) {
1174
+ return new Date(request.deadlineAt).getTime() <= Date.now();
1175
+ }
1176
+ export async function spawnWithShutdownGuard(isShuttingDown, spawn, kill) {
1177
+ if (isShuttingDown())
1178
+ return false;
1179
+ await spawn();
1180
+ if (!isShuttingDown())
1181
+ return true;
1182
+ kill();
1183
+ return false;
1184
+ }
1185
+ function identityMatchesLease(identity, lease) {
1186
+ return (identity.generation === lease.generation &&
1187
+ (identity.deploymentOrder ?? 1) === lease.deploymentOrder &&
1188
+ identity.supervisorId === lease.supervisorId &&
1189
+ identity.shardId === lease.shardId &&
1190
+ identity.fencingToken === lease.fencingToken);
1191
+ }
1192
+ function sameShardLease(left, right) {
1193
+ return (left.generation === right.generation &&
1194
+ left.deploymentOrder === right.deploymentOrder &&
1195
+ left.supervisorId === right.supervisorId &&
1196
+ left.shardId === right.shardId &&
1197
+ left.fencingToken === right.fencingToken);
1198
+ }
1199
+ export async function nextShardDeploymentOrder(database) {
1200
+ let value;
1201
+ try {
1202
+ value = await new RuntimeStateRepository(database).nextShardDeploymentOrder();
1203
+ }
1204
+ catch (error) {
1205
+ if (error.code !== "42P01")
1206
+ throw error;
1207
+ return 1;
1208
+ }
1209
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
1210
+ throw new Error("Shard deployment order exceeds the safe runtime range");
1211
+ }
1212
+ return Number(value);
1213
+ }
1214
+ export async function renewStartupOwnership(coordinator, descriptor, deploymentOrder, leaderLease) {
1215
+ if (!(await coordinator.refreshGeneration(descriptor, deploymentOrder)))
1216
+ throw new Error("Shard generation ownership was lost during startup");
1217
+ if (leaderLease && !(await coordinator.renewLeader(leaderLease)))
1218
+ throw new Error("Shard leader ownership was lost during startup");
1219
+ }
1220
+ function botVersion() {
1221
+ const metadata = createRequire(import.meta.url)("../package.json");
1222
+ if (typeof metadata !== "object" ||
1223
+ metadata === null ||
1224
+ !("version" in metadata) ||
1225
+ typeof metadata.version !== "string") {
1226
+ throw new Error("The bot package version is invalid");
1227
+ }
1228
+ return metadata.version;
1229
+ }
1230
+ //# sourceMappingURL=shard-supervisor.js.map