@helyx/bot 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/control-server.js +2 -1
- package/dist/discord-owned-message-service.js +9 -1
- package/dist/events.js +10 -4
- package/dist/interactions.js +88 -12
- package/dist/scheduled-task-runner.js +11 -5
- package/dist/shard-contracts.d.ts +4 -4
- package/dist/shard-contracts.js +2 -1
- package/dist/shard-supervisor.d.ts +3 -0
- package/dist/shard-supervisor.js +19 -11
- package/dist/shard-worker.js +9 -2
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Polish the Stage 4-6 resource dashboard and Discord flows, add durable
|
|
8
|
+
reaction-role controls, repair poll and giveaway lifecycle handling, and
|
|
9
|
+
strengthen shard-safe scheduling, retention, permissions and resource
|
|
10
|
+
inspection.
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @helyx/control-protocol@0.1.5
|
|
13
|
+
- @helyx/module-manifest@0.4.2
|
|
14
|
+
- @helyx/database@0.4.3
|
|
15
|
+
- @helyx/sdk@0.3.4
|
|
16
|
+
- @helyx/platform@0.2.7
|
|
17
|
+
- @helyx/core@0.1.9
|
|
18
|
+
- @helyx/framework@0.1.9
|
|
19
|
+
|
|
20
|
+
## 0.4.8
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies
|
|
25
|
+
- @helyx/platform@0.2.6
|
|
26
|
+
|
|
3
27
|
## 0.4.7
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/dist/control-server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
|
|
|
10
10
|
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
11
11
|
const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
|
|
12
12
|
const configurationValue = z.record(z.string(), z.union([
|
|
13
|
+
z.null(),
|
|
13
14
|
z.string(),
|
|
14
15
|
z.boolean(),
|
|
15
16
|
z.number().finite(),
|
|
@@ -1099,7 +1100,7 @@ export async function createBotControlServer(input) {
|
|
|
1099
1100
|
const body = mutationContext
|
|
1100
1101
|
.extend({
|
|
1101
1102
|
expectedRevision: z.number().int().min(0),
|
|
1102
|
-
value: managedResourceObject,
|
|
1103
|
+
value: managedResourceObject.optional().default({}),
|
|
1103
1104
|
})
|
|
1104
1105
|
.strict()
|
|
1105
1106
|
.parse(request.body);
|
|
@@ -21,7 +21,7 @@ export class DiscordOwnedMessageDeletionService {
|
|
|
21
21
|
return { cleared: false };
|
|
22
22
|
const reaction = message.reactions.resolve(emoji.cacheKey);
|
|
23
23
|
if (!reaction)
|
|
24
|
-
return { cleared:
|
|
24
|
+
return { cleared: true };
|
|
25
25
|
await reaction.remove();
|
|
26
26
|
return { cleared: true };
|
|
27
27
|
}
|
|
@@ -108,6 +108,14 @@ function prepareEmojiIdentifier(value) {
|
|
|
108
108
|
if (value.length < 1 || value.length > 128 || /[\r\n]/u.test(value)) {
|
|
109
109
|
throw new Error("Invalid bounded Discord emoji identifier");
|
|
110
110
|
}
|
|
111
|
+
const canonicalCustom = /^custom:(\d{17,20})$/u.exec(value);
|
|
112
|
+
if (canonicalCustom) {
|
|
113
|
+
const id = canonicalCustom[1];
|
|
114
|
+
return { identifier: id, cacheKey: id };
|
|
115
|
+
}
|
|
116
|
+
const canonicalUnicode = /^unicode:(.+)$/u.exec(value);
|
|
117
|
+
if (canonicalUnicode)
|
|
118
|
+
value = canonicalUnicode[1];
|
|
111
119
|
const custom = /^(?:<a?:([A-Za-z0-9_]{2,32}):(\d{17,20})>|([A-Za-z0-9_]{2,32}):(\d{17,20}))$/u.exec(value);
|
|
112
120
|
if (custom) {
|
|
113
121
|
const name = custom[1] ?? custom[3];
|
package/dist/events.js
CHANGED
|
@@ -574,9 +574,13 @@ export class DiscordResourceInspection {
|
|
|
574
574
|
const guild = await this.client.guilds.fetch(input.guildId);
|
|
575
575
|
if (guild.id !== input.guildId)
|
|
576
576
|
throw new Error("Discord guild validation failed");
|
|
577
|
+
const canonicalCustom = /^custom:(\d{17,20})$/u.exec(input.emojiIdentifier);
|
|
578
|
+
const canonicalUnicode = /^unicode:(.+)$/u.exec(input.emojiIdentifier);
|
|
577
579
|
const custom = customEmojiIdentifier.exec(input.emojiIdentifier);
|
|
578
|
-
if (!custom) {
|
|
579
|
-
const normalized = input.emojiIdentifier
|
|
580
|
+
if (!custom && !canonicalCustom) {
|
|
581
|
+
const normalized = (canonicalUnicode?.[1] ?? input.emojiIdentifier)
|
|
582
|
+
.normalize("NFKC")
|
|
583
|
+
.trim();
|
|
580
584
|
const valid = isSingleUnicodeEmoji(normalized);
|
|
581
585
|
return {
|
|
582
586
|
guildId: guild.id,
|
|
@@ -590,7 +594,9 @@ export class DiscordResourceInspection {
|
|
|
590
594
|
: {}),
|
|
591
595
|
};
|
|
592
596
|
}
|
|
593
|
-
const emojiId = custom[2] ?? custom[4];
|
|
597
|
+
const emojiId = canonicalCustom?.[1] ?? custom?.[2] ?? custom?.[4];
|
|
598
|
+
if (!emojiId)
|
|
599
|
+
throw new Error("Invalid bounded Discord emoji identifier");
|
|
594
600
|
const emoji = await guild.emojis.fetch(emojiId).catch(() => null);
|
|
595
601
|
if (!emoji)
|
|
596
602
|
return {
|
|
@@ -612,7 +618,7 @@ export class DiscordResourceInspection {
|
|
|
612
618
|
kind: "custom",
|
|
613
619
|
canonicalEmojiKey: `custom:${emoji.id}`,
|
|
614
620
|
emojiId: emoji.id,
|
|
615
|
-
name: emoji.name ?? custom[1] ?? custom[3] ?? "emoji",
|
|
621
|
+
name: emoji.name ?? custom?.[1] ?? custom?.[3] ?? "emoji",
|
|
616
622
|
};
|
|
617
623
|
}
|
|
618
624
|
async inspectChannel(input) {
|
package/dist/interactions.js
CHANGED
|
@@ -67,7 +67,24 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
67
67
|
});
|
|
68
68
|
async function handleInteraction(interaction) {
|
|
69
69
|
try {
|
|
70
|
-
if (interaction.
|
|
70
|
+
if (interaction.isAutocomplete()) {
|
|
71
|
+
const registered = commands.get(interaction.commandName);
|
|
72
|
+
const subcommandName = registered?.contribution.subcommands?.length
|
|
73
|
+
? interaction.options.getSubcommand(false)
|
|
74
|
+
: null;
|
|
75
|
+
const subcommand = subcommandName
|
|
76
|
+
? registered?.contribution.subcommands?.find(({ name }) => name === subcommandName)
|
|
77
|
+
: undefined;
|
|
78
|
+
if (!registered ||
|
|
79
|
+
!subcommand?.autocomplete ||
|
|
80
|
+
!(await authorizeAutocomplete(interaction, registered.moduleId, subcommand.permissionId ??
|
|
81
|
+
`${registered.contribution.name}.${subcommand.name}`, services, registration.selfAuthorizingGuildModuleIds?.has(registered.moduleId) ?? false, subcommand.authorization === "resource-scoped"))) {
|
|
82
|
+
await interaction.respond([]);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await subcommand.autocomplete(createAutocompleteContext(interaction, services));
|
|
86
|
+
}
|
|
87
|
+
else if (interaction.isChatInputCommand()) {
|
|
71
88
|
const registered = commands.get(interaction.commandName);
|
|
72
89
|
if (registered) {
|
|
73
90
|
const { contribution: command, moduleId } = registered;
|
|
@@ -217,6 +234,8 @@ export function toCommandDefinition(registered) {
|
|
|
217
234
|
}
|
|
218
235
|
function addCommandOption(builder, option) {
|
|
219
236
|
if (option.type === "string") {
|
|
237
|
+
if (option.autocomplete && option.choices?.length)
|
|
238
|
+
throw new Error(`Command option ${option.name} cannot use choices and autocomplete together`);
|
|
220
239
|
builder.addStringOption((optionBuilder) => {
|
|
221
240
|
optionBuilder
|
|
222
241
|
.setName(option.name)
|
|
@@ -228,6 +247,8 @@ function addCommandOption(builder, option) {
|
|
|
228
247
|
optionBuilder.setMaxLength(option.maxLength);
|
|
229
248
|
if (option.choices)
|
|
230
249
|
optionBuilder.addChoices(...option.choices);
|
|
250
|
+
if (option.autocomplete)
|
|
251
|
+
optionBuilder.setAutocomplete(option.autocomplete);
|
|
231
252
|
return optionBuilder;
|
|
232
253
|
});
|
|
233
254
|
return;
|
|
@@ -335,6 +356,41 @@ function createCommandContext(interaction, services) {
|
|
|
335
356
|
},
|
|
336
357
|
};
|
|
337
358
|
}
|
|
359
|
+
function createAutocompleteContext(interaction, services) {
|
|
360
|
+
const focused = interaction.options.getFocused(true);
|
|
361
|
+
const actor = permissionActor(interaction);
|
|
362
|
+
return {
|
|
363
|
+
interactionId: interaction.id,
|
|
364
|
+
userId: actor.userId,
|
|
365
|
+
userRoleIds: actor.roleIds,
|
|
366
|
+
userPermissions: actor.permissions,
|
|
367
|
+
isServerOwner: actor.isServerOwner,
|
|
368
|
+
isAdministrator: actor.isAdministrator,
|
|
369
|
+
...(interaction.guildId ? { serverId: interaction.guildId } : {}),
|
|
370
|
+
focusedOptionName: focused.name,
|
|
371
|
+
focusedValue: String(focused.value),
|
|
372
|
+
services,
|
|
373
|
+
respond: async (choices) => {
|
|
374
|
+
if (choices.length > 25)
|
|
375
|
+
throw new Error("Autocomplete supports at most 25 choices");
|
|
376
|
+
const values = new Set();
|
|
377
|
+
const normalized = choices.map(({ name, value }) => {
|
|
378
|
+
const choiceName = name.trim().normalize("NFC");
|
|
379
|
+
const choiceValue = value.trim().normalize("NFC");
|
|
380
|
+
if (choiceName.length < 1 ||
|
|
381
|
+
choiceName.length > 100 ||
|
|
382
|
+
choiceValue.length < 1 ||
|
|
383
|
+
choiceValue.length > 100)
|
|
384
|
+
throw new Error("Autocomplete names and values must be between 1 and 100 characters");
|
|
385
|
+
if (values.has(choiceValue))
|
|
386
|
+
throw new Error("Autocomplete choice values must be unique");
|
|
387
|
+
values.add(choiceValue);
|
|
388
|
+
return { name: choiceName, value: choiceValue };
|
|
389
|
+
});
|
|
390
|
+
await interaction.respond(normalized);
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
338
394
|
function createModalContext(interaction, services) {
|
|
339
395
|
const updateResponse = interaction.isFromMessage()
|
|
340
396
|
? async (response) => interaction.update(toUpdateOptions(response)).then(() => undefined)
|
|
@@ -467,17 +523,7 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
467
523
|
if (!resourceScopedCommand) {
|
|
468
524
|
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
469
525
|
const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
|
|
470
|
-
const actor =
|
|
471
|
-
userId: interaction.user.id,
|
|
472
|
-
roleIds: interaction.member instanceof GuildMember
|
|
473
|
-
? [...interaction.member.roles.cache.keys()]
|
|
474
|
-
: (interaction.member?.roles ?? []),
|
|
475
|
-
permissions: interaction.memberPermissions
|
|
476
|
-
?.toArray()
|
|
477
|
-
.map(normalizeDiscordPermission) ?? [],
|
|
478
|
-
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
479
|
-
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ?? false,
|
|
480
|
-
};
|
|
526
|
+
const actor = permissionActor(interaction);
|
|
481
527
|
if (!canUseCommand(actor, policy)) {
|
|
482
528
|
await interaction.reply({
|
|
483
529
|
content: "You do not have permission to use this command.",
|
|
@@ -506,6 +552,32 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
506
552
|
}
|
|
507
553
|
return true;
|
|
508
554
|
}
|
|
555
|
+
async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand) {
|
|
556
|
+
if (!interaction.guildId || selfAuthorizingGuildCommand)
|
|
557
|
+
return true;
|
|
558
|
+
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
559
|
+
if (!(await installations.isModuleEnabled(interaction.guildId, moduleId)))
|
|
560
|
+
return false;
|
|
561
|
+
if (resourceScopedCommand)
|
|
562
|
+
return true;
|
|
563
|
+
const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
564
|
+
const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
|
|
565
|
+
return canUseCommand(permissionActor(interaction), policy);
|
|
566
|
+
}
|
|
567
|
+
function permissionActor(interaction) {
|
|
568
|
+
return {
|
|
569
|
+
userId: interaction.user.id,
|
|
570
|
+
roleIds: interaction.member instanceof GuildMember
|
|
571
|
+
? [...interaction.member.roles.cache.keys()]
|
|
572
|
+
: (interaction.member?.roles ?? []),
|
|
573
|
+
permissions: interaction.memberPermissions
|
|
574
|
+
?.toArray()
|
|
575
|
+
.map(normalizeDiscordPermission) ?? [],
|
|
576
|
+
isServerOwner: interaction.guild?.ownerId === interaction.user.id,
|
|
577
|
+
isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ??
|
|
578
|
+
false,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
509
581
|
export function normalizeDiscordPermission(permission) {
|
|
510
582
|
return permission.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
511
583
|
}
|
|
@@ -760,6 +832,10 @@ export function toModal(definition) {
|
|
|
760
832
|
return modal;
|
|
761
833
|
}
|
|
762
834
|
async function sendFailure(interaction) {
|
|
835
|
+
if (interaction.isAutocomplete()) {
|
|
836
|
+
await interaction.respond([]).catch(() => undefined);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
763
839
|
if (!interaction.isRepliable())
|
|
764
840
|
return;
|
|
765
841
|
const message = "Something went wrong while handling that interaction.";
|
|
@@ -306,11 +306,17 @@ export function createLocalScheduledTaskRoute(input) {
|
|
|
306
306
|
.execute(retentionTaskId);
|
|
307
307
|
return result.completed
|
|
308
308
|
? { outcome: "completed" }
|
|
309
|
-
:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
309
|
+
: result.deferredUntil
|
|
310
|
+
? {
|
|
311
|
+
outcome: "deferred",
|
|
312
|
+
delayMs: Math.max(100, Math.min(86_400_000, result.deferredUntil.getTime() - Date.now())),
|
|
313
|
+
safeCode: "retention_rescheduled",
|
|
314
|
+
}
|
|
315
|
+
: {
|
|
316
|
+
outcome: "deferred",
|
|
317
|
+
delayMs: 100,
|
|
318
|
+
safeCode: "retention_batch_remaining",
|
|
319
|
+
};
|
|
314
320
|
}
|
|
315
321
|
const module = modules.get(task.moduleId);
|
|
316
322
|
if (!module)
|
|
@@ -3,7 +3,7 @@ import type { ManagedResourceValue } from "@helyx/sdk";
|
|
|
3
3
|
import type { BotControlHandler } from "./control-server.js";
|
|
4
4
|
export declare const SHARD_CONTRACT_VERSION: 1;
|
|
5
5
|
export declare const SHARD_CONTROL_MAX_BYTES: number;
|
|
6
|
-
export declare const SHARD_CONTROL_TIMEOUT_MS =
|
|
6
|
+
export declare const SHARD_CONTROL_TIMEOUT_MS = 30000;
|
|
7
7
|
export declare const SHARD_HEALTH_STALE_MS = 30000;
|
|
8
8
|
export declare const SHARD_LEASE_HEARTBEAT_TIMEOUT_MS = 20000;
|
|
9
9
|
export declare const shardIdentitySchema: z.ZodObject<{
|
|
@@ -182,7 +182,7 @@ export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.Zod
|
|
|
182
182
|
kind: z.ZodLiteral<"guild.module.configuration.update">;
|
|
183
183
|
guildId: z.ZodString;
|
|
184
184
|
moduleId: z.ZodString;
|
|
185
|
-
value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
|
|
185
|
+
value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNull, z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
|
|
186
186
|
expectedVersion: z.ZodNullable<z.ZodNumber>;
|
|
187
187
|
}, z.core.$strict>, z.ZodObject<{
|
|
188
188
|
actorUserId: z.ZodString;
|
|
@@ -380,7 +380,7 @@ export declare const shardControlRequestSchema: z.ZodObject<{
|
|
|
380
380
|
kind: z.ZodLiteral<"guild.module.configuration.update">;
|
|
381
381
|
guildId: z.ZodString;
|
|
382
382
|
moduleId: z.ZodString;
|
|
383
|
-
value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
|
|
383
|
+
value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNull, z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
|
|
384
384
|
expectedVersion: z.ZodNullable<z.ZodNumber>;
|
|
385
385
|
}, z.core.$strict>, z.ZodObject<{
|
|
386
386
|
actorUserId: z.ZodString;
|
|
@@ -838,7 +838,7 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
|
|
|
838
838
|
kind: z.ZodLiteral<"guild.module.configuration.update">;
|
|
839
839
|
guildId: z.ZodString;
|
|
840
840
|
moduleId: z.ZodString;
|
|
841
|
-
value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
|
|
841
|
+
value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNull, z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
|
|
842
842
|
expectedVersion: z.ZodNullable<z.ZodNumber>;
|
|
843
843
|
}, z.core.$strict>, z.ZodObject<{
|
|
844
844
|
actorUserId: z.ZodString;
|
package/dist/shard-contracts.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const SHARD_CONTRACT_VERSION = 1;
|
|
3
3
|
export const SHARD_CONTROL_MAX_BYTES = 64 * 1_024;
|
|
4
|
-
export const SHARD_CONTROL_TIMEOUT_MS =
|
|
4
|
+
export const SHARD_CONTROL_TIMEOUT_MS = 30_000;
|
|
5
5
|
export const SHARD_HEALTH_STALE_MS = 30_000;
|
|
6
6
|
export const SHARD_LEASE_HEARTBEAT_TIMEOUT_MS = 20_000;
|
|
7
7
|
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
@@ -43,6 +43,7 @@ const permissionState = z
|
|
|
43
43
|
})
|
|
44
44
|
.strict();
|
|
45
45
|
const configurationValue = z.record(z.string().min(1).max(100), z.union([
|
|
46
|
+
z.null(),
|
|
46
47
|
z.string().max(4_000),
|
|
47
48
|
z.boolean(),
|
|
48
49
|
z.number().finite(),
|
|
@@ -5,6 +5,9 @@ import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } f
|
|
|
5
5
|
import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
|
|
6
6
|
import type { BotHandle } from "./index.js";
|
|
7
7
|
export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
|
|
8
|
+
export declare function createStartupWorkerHeartbeatMaintenance(heartbeat: () => void, intervalMs?: number): {
|
|
9
|
+
stop(): void;
|
|
10
|
+
};
|
|
8
11
|
export declare function deferScheduledRoutingFailures(route: () => Promise<ScheduledTaskExecutionResult>): Promise<ScheduledTaskExecutionResult>;
|
|
9
12
|
export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
|
|
10
13
|
export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
|
package/dist/shard-supervisor.js
CHANGED
|
@@ -31,6 +31,13 @@ const WORKER_SHUTDOWN_MS = 5_000;
|
|
|
31
31
|
const MAX_RESTARTS_PER_WINDOW = 5;
|
|
32
32
|
const RESTART_WINDOW_MS = 10 * 60 * 1_000;
|
|
33
33
|
export const STEADY_STATE_LEASE_LOSS_BOUND_MS = LEASE_RENEW_INTERVAL_MS + LEASE_VALIDATION_TIMEOUT_MS + WORKER_SHUTDOWN_MS;
|
|
34
|
+
export function createStartupWorkerHeartbeatMaintenance(heartbeat, intervalMs = STARTUP_LEASE_RENEW_INTERVAL_MS) {
|
|
35
|
+
const timer = setInterval(heartbeat, intervalMs);
|
|
36
|
+
timer.unref();
|
|
37
|
+
return {
|
|
38
|
+
stop: () => clearInterval(timer),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
34
41
|
export async function deferScheduledRoutingFailures(route) {
|
|
35
42
|
try {
|
|
36
43
|
return await route();
|
|
@@ -73,6 +80,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
73
80
|
let retentionTimer;
|
|
74
81
|
let leaseTimer;
|
|
75
82
|
let startupLeaseTimer;
|
|
83
|
+
let startupWorkerHeartbeats;
|
|
76
84
|
let startupLeaseRefresh;
|
|
77
85
|
let startupLeaseError;
|
|
78
86
|
let shuttingDown = false;
|
|
@@ -297,6 +305,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
297
305
|
execArgv: process.execArgv,
|
|
298
306
|
});
|
|
299
307
|
await reserveInitialSessionStarts(claimedIds.length, gateway);
|
|
308
|
+
if (coordinator) {
|
|
309
|
+
startupWorkerHeartbeats = createStartupWorkerHeartbeatMaintenance(() => {
|
|
310
|
+
void Promise.all([...localLeases].map(([shardId, lease]) => sendLeaseHeartbeat(shardId, lease)));
|
|
311
|
+
});
|
|
312
|
+
}
|
|
300
313
|
await spawnInIdentifyBuckets(claimedIds, gateway.session_start_limit.max_concurrency);
|
|
301
314
|
workersRoutable = true;
|
|
302
315
|
if (leaderLease)
|
|
@@ -317,8 +330,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
317
330
|
await stopStartupLeaseMaintenance();
|
|
318
331
|
if (coordinator)
|
|
319
332
|
await withTimeout(renewStartupOwnership(coordinator, generationDescriptor, deploymentOrder, leaderLease), LEASE_VALIDATION_TIMEOUT_MS);
|
|
333
|
+
await Promise.all([...localLeases].map(([shardId, lease]) => sendLeaseHeartbeat(shardId, lease)));
|
|
320
334
|
leaseTimer = setInterval(() => void maintainLeases(), LEASE_RENEW_INTERVAL_MS);
|
|
321
335
|
leaseTimer.unref();
|
|
336
|
+
startupWorkerHeartbeats?.stop();
|
|
337
|
+
startupWorkerHeartbeats = undefined;
|
|
322
338
|
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
323
339
|
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
324
340
|
logger.info({
|
|
@@ -414,17 +430,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
414
430
|
};
|
|
415
431
|
shard.env = buildShardWorkerEnvironment(processEnvironment, identity, restartCounts.get(shardId) ?? 0);
|
|
416
432
|
healthByShard.set(shardId, startingHealth(shardId, restartCounts.get(shardId) ?? 0));
|
|
417
|
-
|
|
418
|
-
? setInterval(() => void sendLeaseHeartbeat(shardId, lease), STARTUP_LEASE_RENEW_INTERVAL_MS)
|
|
419
|
-
: undefined;
|
|
420
|
-
launchHeartbeatTimer?.unref();
|
|
421
|
-
try {
|
|
422
|
-
await spawnWithShutdownGuard(() => shuttingDown, () => shard.spawn(30_000), () => shard.kill());
|
|
423
|
-
}
|
|
424
|
-
finally {
|
|
425
|
-
if (launchHeartbeatTimer)
|
|
426
|
-
clearInterval(launchHeartbeatTimer);
|
|
427
|
-
}
|
|
433
|
+
await spawnWithShutdownGuard(() => shuttingDown, () => shard.spawn(30_000), () => shard.kill());
|
|
428
434
|
await sendLeaseHeartbeat(shardId, lease);
|
|
429
435
|
}
|
|
430
436
|
async function sendLeaseHeartbeat(shardId, lease) {
|
|
@@ -985,6 +991,8 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
985
991
|
clearInterval(leaseTimer);
|
|
986
992
|
if (startupLeaseTimer)
|
|
987
993
|
clearInterval(startupLeaseTimer);
|
|
994
|
+
startupWorkerHeartbeats?.stop();
|
|
995
|
+
startupWorkerHeartbeats = undefined;
|
|
988
996
|
if (retentionTimer)
|
|
989
997
|
clearInterval(retentionTimer);
|
|
990
998
|
workersRoutable = false;
|
package/dist/shard-worker.js
CHANGED
|
@@ -207,7 +207,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
207
207
|
if (!parsed.success)
|
|
208
208
|
return;
|
|
209
209
|
if (parsed.data.type === "helyx.control.request") {
|
|
210
|
-
void handleControl(identity, controlService, parsed.data);
|
|
210
|
+
void handleControl(identity, controlService, parsed.data, logger);
|
|
211
211
|
}
|
|
212
212
|
else if (parsed.data.type === "helyx.lease.heartbeat") {
|
|
213
213
|
if (sameShardIdentity(parsed.data.identity, identity))
|
|
@@ -390,7 +390,7 @@ export function shardWorkerCommandRegistration(input) {
|
|
|
390
390
|
]),
|
|
391
391
|
};
|
|
392
392
|
}
|
|
393
|
-
async function handleControl(identity, service, request) {
|
|
393
|
+
async function handleControl(identity, service, request, logger) {
|
|
394
394
|
let response;
|
|
395
395
|
const guildId = controlOperationGuildId(request.operation);
|
|
396
396
|
if (request.generation !== identity.generation ||
|
|
@@ -418,6 +418,13 @@ async function handleControl(identity, service, request) {
|
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
420
|
catch (error) {
|
|
421
|
+
logger.error({
|
|
422
|
+
event: "shard.control_failed",
|
|
423
|
+
requestId: request.requestId,
|
|
424
|
+
operation: request.operation.kind,
|
|
425
|
+
guildId,
|
|
426
|
+
error: safeError(error),
|
|
427
|
+
}, "Shard control operation failed");
|
|
421
428
|
const mapped = mapBotControlError(error);
|
|
422
429
|
response = {
|
|
423
430
|
version: 1,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/bot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Production Discord runtime for the modular Helyx platform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@helyx/config": "0.4.2",
|
|
52
|
-
"@helyx/control-protocol": "0.1.
|
|
53
|
-
"@helyx/core": "0.1.
|
|
54
|
-
"@helyx/database": "0.4.
|
|
55
|
-
"@helyx/framework": "0.1.
|
|
56
|
-
"@helyx/module-manifest": "0.4.
|
|
57
|
-
"@helyx/platform": "0.2.
|
|
58
|
-
"@helyx/sdk": "0.3.
|
|
52
|
+
"@helyx/control-protocol": "0.1.5",
|
|
53
|
+
"@helyx/core": "0.1.9",
|
|
54
|
+
"@helyx/database": "0.4.3",
|
|
55
|
+
"@helyx/framework": "0.1.9",
|
|
56
|
+
"@helyx/module-manifest": "0.4.2",
|
|
57
|
+
"@helyx/platform": "0.2.7",
|
|
58
|
+
"@helyx/sdk": "0.3.4",
|
|
59
59
|
"discord.js": "14.27.0",
|
|
60
60
|
"fastify": "5.10.0",
|
|
61
61
|
"pino": "10.3.1",
|