@helyx/bot 0.4.33 → 0.4.36

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 (40) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cloud-management-module.js +7 -1
  3. package/dist/control-server.d.ts +6 -7
  4. package/dist/control-server.js +38 -45
  5. package/dist/dashboard-commands.d.ts +10 -0
  6. package/dist/dashboard-commands.js +47 -0
  7. package/dist/developer-operations-module.js +5 -1
  8. package/dist/discord-guild-health.d.ts +4 -3
  9. package/dist/discord-guild-health.js +6 -3
  10. package/dist/events.js +38 -16
  11. package/dist/index.js +43 -3
  12. package/dist/interactions.d.ts +3 -2
  13. package/dist/interactions.js +132 -45
  14. package/dist/internal-module-manifest.d.ts +13 -0
  15. package/dist/internal-module-manifest.js +14 -0
  16. package/dist/management-module.js +7 -2
  17. package/dist/module-availability-control.d.ts +24 -0
  18. package/dist/module-availability-control.js +61 -0
  19. package/dist/modules.js +24 -1
  20. package/dist/nexus-commands.d.ts +13 -0
  21. package/dist/nexus-commands.js +100 -0
  22. package/dist/numeric-barriers.d.ts +11 -0
  23. package/dist/numeric-barriers.js +14 -0
  24. package/dist/numeric-write-behind-proxy.d.ts +4 -4
  25. package/dist/numeric-write-behind-proxy.js +45 -24
  26. package/dist/numeric-write-behind-redis.d.ts +4 -4
  27. package/dist/numeric-write-behind-redis.js +132 -70
  28. package/dist/oauth-module.js +3 -1
  29. package/dist/permissions-module.js +18 -5
  30. package/dist/renewable-lease.d.ts +16 -0
  31. package/dist/renewable-lease.js +66 -0
  32. package/dist/scheduled-task-runner.js +15 -1
  33. package/dist/shard-router.d.ts +2 -0
  34. package/dist/shard-router.js +5 -0
  35. package/dist/shard-supervisor.d.ts +5 -0
  36. package/dist/shard-supervisor.js +63 -4
  37. package/dist/shard-worker.d.ts +7 -1
  38. package/dist/shard-worker.js +4 -1
  39. package/dist/support-module.js +3 -1
  40. package/package.json +5 -4
@@ -23,6 +23,7 @@ import { createDeveloperOperationsModule } from "./developer-operations-module.j
23
23
  import { SupervisorCloudDiscordDirectory } from "./supervisor-cloud-directory.js";
24
24
  import { createSupportModule } from "./support-module.js";
25
25
  import { createOAuthModule } from "./oauth-module.js";
26
+ import { availabilityCatalogue, availabilityCatalogueDefinitions, } from "./module-availability-control.js";
26
27
  import { createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
27
28
  import { numericWriteBehindRegistrations } from "./numeric-write-behind.js";
28
29
  import { executeRedisNumericOperation, RedisNumericWriteBehindService, } from "./numeric-write-behind-redis.js";
@@ -56,6 +57,13 @@ export function createStartupWorkerHeartbeatMaintenance(heartbeat, intervalMs =
56
57
  stop: () => clearInterval(timer),
57
58
  };
58
59
  }
60
+ export function relinquishRestartCircuitOwnership(input) {
61
+ const lease = input.localLeases.get(input.shardId);
62
+ if (!lease)
63
+ return Promise.resolve(false);
64
+ input.localLeases.delete(input.shardId);
65
+ return input.release(lease);
66
+ }
59
67
  export async function deferScheduledRoutingFailures(route) {
60
68
  try {
61
69
  return await route();
@@ -119,6 +127,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
119
127
  const pendingNumericCommitted = new Map();
120
128
  const restartHistory = new Map();
121
129
  const restartCounts = new Map();
130
+ const openRestartCircuits = new Set();
122
131
  const workers = new Map();
123
132
  let sessionBudget;
124
133
  let databaseHealthy = false;
@@ -229,6 +238,10 @@ export async function startShardSupervisor(environment, processEnvironment, work
229
238
  ...publicModules,
230
239
  ...(operationsModule ? [operationsModule] : []),
231
240
  ];
241
+ const availabilityModules = availabilityCatalogueDefinitions(modules);
242
+ const availabilityService = {
243
+ listAvailabilityModules: () => availabilityCatalogue(availabilityModules),
244
+ };
232
245
  const initialiseLeader = async () => {
233
246
  await runLeaderStartup(database, environment, modulePackages, modules, logger);
234
247
  assertStartupLeaseHealthy();
@@ -258,6 +271,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
258
271
  localLeases,
259
272
  dispatchLocal,
260
273
  privacy: privacyService,
274
+ availability: availabilityService,
261
275
  localLeaderLease: leaderLease,
262
276
  ...(coordinator ? { coordinator } : {}),
263
277
  ...(environment.DISCORD_INTERNAL_GUILD_ID
@@ -351,6 +365,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
351
365
  localLeases,
352
366
  dispatchLocal,
353
367
  ...(privacyService ? { privacy: privacyService } : {}),
368
+ availability: availabilityService,
354
369
  ...(leaderLease ? { localLeaderLease: leaderLease } : {}),
355
370
  ...(coordinator ? { coordinator } : {}),
356
371
  ...(environment.DISCORD_INTERNAL_GUILD_ID
@@ -526,8 +541,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
526
541
  }
527
542
  function installNumericCommitRouting(service) {
528
543
  for (const registration of numericRegistrations) {
529
- service.subscribeCommitted(registration, async (batch) => {
544
+ service.subscribeCommitted(registration, async (batch, signal) => {
530
545
  for (let index = 0; index < batch.mutations.length; index += 1) {
546
+ signal.throwIfAborted();
531
547
  const mutation = batch.mutations[index];
532
548
  const result = batch.results[index];
533
549
  const guildId = mutation?.identity.guild_id;
@@ -563,32 +579,53 @@ export async function startShardSupervisor(environment, processEnvironment, work
563
579
  payload: encodeNumericIpcValue(committedItem),
564
580
  };
565
581
  assertShardMessageSize(notification);
566
- const response = await dispatchNumericCommitted(shardId, identity, notification);
582
+ const response = await dispatchNumericCommitted(shardId, identity, notification, signal);
567
583
  if (!response.ok)
568
584
  throw new Error(`Committed numeric batch was not applied: ${response.safeCode}`);
569
585
  }
570
586
  });
571
587
  }
572
588
  }
573
- function dispatchNumericCommitted(shardId, identity, notification) {
589
+ function dispatchNumericCommitted(shardId, identity, notification, signal) {
574
590
  const worker = workers.get(shardId);
575
591
  if (!worker?.process)
576
592
  return Promise.reject(new Error("Committed numeric batch owner is unavailable"));
577
593
  return new Promise((resolve, reject) => {
594
+ const onAbort = () => {
595
+ const pending = pendingNumericCommitted.get(notification.requestId);
596
+ if (!pending)
597
+ return;
598
+ pendingNumericCommitted.delete(notification.requestId);
599
+ clearTimeout(pending.timer);
600
+ pending.removeAbortListener();
601
+ reject(signal.reason instanceof Error
602
+ ? signal.reason
603
+ : new Error("Numeric claim ownership was lost"));
604
+ };
578
605
  const timer = setTimeout(() => {
606
+ const pending = pendingNumericCommitted.get(notification.requestId);
579
607
  pendingNumericCommitted.delete(notification.requestId);
608
+ pending?.removeAbortListener();
580
609
  reject(new Error("Committed numeric batch acknowledgement timed out"));
581
610
  }, SHARD_CONTROL_TIMEOUT_MS);
582
611
  timer.unref();
612
+ signal.addEventListener("abort", onAbort, { once: true });
583
613
  pendingNumericCommitted.set(notification.requestId, {
584
614
  shardId,
585
615
  identity,
586
616
  resolve,
587
617
  timer,
618
+ removeAbortListener: () => signal.removeEventListener("abort", onAbort),
588
619
  });
620
+ if (signal.aborted) {
621
+ onAbort();
622
+ return;
623
+ }
589
624
  void worker.send(notification).catch((error) => {
590
625
  clearTimeout(timer);
626
+ const pending = pendingNumericCommitted.get(notification.requestId);
591
627
  pendingNumericCommitted.delete(notification.requestId);
628
+ pending?.removeAbortListener();
592
629
  reject(error instanceof Error
593
630
  ? error
594
631
  : new Error("Committed numeric delivery failed"));
@@ -713,6 +750,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
713
750
  return;
714
751
  pendingNumericCommitted.delete(message.data.requestId);
715
752
  clearTimeout(pending.timer);
753
+ pending.removeAbortListener();
716
754
  pending.resolve(message.data);
717
755
  return;
718
756
  }
@@ -902,6 +940,25 @@ export async function startShardSupervisor(environment, processEnvironment, work
902
940
  const now = Date.now();
903
941
  const recent = (restartHistory.get(shardId) ?? []).filter((time) => now - time < RESTART_WINDOW_MS);
904
942
  if (recent.length >= MAX_RESTARTS_PER_WINDOW) {
943
+ openRestartCircuits.add(shardId);
944
+ const circuitCoordinator = coordinator;
945
+ if (circuitCoordinator)
946
+ void relinquishRestartCircuitOwnership({
947
+ shardId,
948
+ localLeases,
949
+ release: (lease) => circuitCoordinator.releaseShard(lease),
950
+ })
951
+ .then((released) => {
952
+ if (!released)
953
+ logger.warn({ event: "shard.circuit_lease_release_stale", shardId }, "Shard restart circuit lease was already reassigned");
954
+ })
955
+ .catch((error) => logger.warn({
956
+ event: "shard.circuit_lease_release_failed",
957
+ shardId,
958
+ error: safeError(error),
959
+ }, "Shard restart circuit lease will expire without renewal"));
960
+ else
961
+ localLeases.delete(shardId);
905
962
  logger.error({ event: "shard.circuit_open", shardId, restartCount: recent.length }, "Shard worker restart circuit opened");
906
963
  return;
907
964
  }
@@ -991,7 +1048,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
991
1048
  try {
992
1049
  const occupied = new Set((await withTimeout(coordinator.getShardLeases(shardCount), LEASE_VALIDATION_TIMEOUT_MS).catch(() => [])).map(({ shardId }) => shardId));
993
1050
  const candidates = Array.from({ length: shardCount }, (_, shardId) => shardId)
994
- .filter((shardId) => !localLeases.has(shardId) && !occupied.has(shardId))
1051
+ .filter((shardId) => !localLeases.has(shardId) &&
1052
+ !occupied.has(shardId) &&
1053
+ !openRestartCircuits.has(shardId))
995
1054
  .slice(0, capacity);
996
1055
  const results = await Promise.all(candidates.map(async (shardId) => {
997
1056
  const lease = await withTimeout(coordinator.claimShard(generationDescriptor, shardId, deploymentOrder), LEASE_VALIDATION_TIMEOUT_MS).catch(() => undefined);
@@ -50,12 +50,15 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
50
50
  HELYX_OAUTH_ENABLED: boolean;
51
51
  HELYX_CLOUD_ENDPOINT: string;
52
52
  HELYX_OWNER_USER_IDS: string[];
53
- HELYX_SHARD_MODE: "off" | "auto" | "fixed";
53
+ HELYX_SHARD_MODE: "fixed" | "off" | "auto";
54
54
  HELYX_SHARD_SCOPE: "local" | "coordinated";
55
55
  HELYX_CLOUD_AUDIT_RETENTION_DAYS: number;
56
56
  HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS: number;
57
57
  LOG_LEVEL: "error" | "fatal" | "warn" | "info" | "debug" | "trace" | "silent";
58
58
  NODE_ENV: "development" | "test" | "production";
59
+ HELYX_STATUS_ENABLED: boolean;
60
+ HELYX_STATUS_URL: string;
61
+ HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS: number;
59
62
  HELYX_BOT_CONTROL_SECRET?: string | undefined;
60
63
  HELYX_SUPPORT_SERVICE_SECRET?: string | undefined;
61
64
  HELYX_VERIFICATION_INSTALLATION_ID?: string | undefined;
@@ -69,6 +72,9 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
69
72
  HELYX_SHARD_REDIS_NAMESPACE?: string | undefined;
70
73
  HELYX_SHARD_GENERATION?: string | undefined;
71
74
  HELYX_SHARD_DATABASE_CONNECTION_BUDGET?: number | undefined;
75
+ HELYX_STATUS_HEARTBEAT_SECRET?: string | undefined;
76
+ CF_ACCESS_CLIENT_ID?: string | undefined;
77
+ CF_ACCESS_CLIENT_SECRET?: string | undefined;
72
78
  };
73
79
  export declare function shardWorkerCommandRegistration(input: {
74
80
  internalGuildId?: string;
@@ -3,7 +3,7 @@ import { safeError, ServiceRegistry } from "@helyx/core";
3
3
  import { checkDatabaseConnection, createDatabasePool } from "@helyx/database";
4
4
  import { createRuntime } from "@helyx/framework";
5
5
  import { createPlatformServices } from "@helyx/platform";
6
- import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
6
+ import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
7
7
  import { Client, Events, Partials } from "discord.js";
8
8
  import { monitorEventLoopDelay } from "node:perf_hooks";
9
9
  import pino from "pino";
@@ -179,6 +179,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
179
179
  })) {
180
180
  platformServices.set(name, service);
181
181
  }
182
+ await serviceAccess
183
+ .get(HELYX_SERVICE_NAMES.moduleAvailability)
184
+ .refresh();
182
185
  if (environment.HELYX_VERIFICATION_ENABLED &&
183
186
  environment.HELYX_VERIFICATION_URL &&
184
187
  environment.HELYX_VERIFICATION_INTERNAL_SECRET) {
@@ -1,5 +1,6 @@
1
1
  import { defineModule, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
3
4
  const MODAL_ID = "helyx.support.bug";
4
5
  const MODULE_FIELD_ID = "helyx.support.bug.module";
5
6
  const SUMMARY_FIELD_ID = "helyx.support.bug.summary";
@@ -11,7 +12,7 @@ const HOUR_SECONDS = 60 * 60;
11
12
  export function createSupportModule(transport) {
12
13
  return defineModule({
13
14
  manifest: {
14
- manifestVersion: "0.1",
15
+ ...INTERNAL_MODULE_MANIFEST_FIELDS,
15
16
  releaseStatus: "live",
16
17
  id: MODULE_ID,
17
18
  name: "Helyx Support",
@@ -28,6 +29,7 @@ export function createSupportModule(transport) {
28
29
  {
29
30
  name: "bug",
30
31
  description: "Report a Helyx bug to the support team",
32
+ acknowledgement: "immediate-modal",
31
33
  registration: "global",
32
34
  contexts: ["guild"],
33
35
  integrationTypes: ["guild_install"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.33",
3
+ "version": "0.4.36",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -53,9 +53,10 @@
53
53
  "@helyx/core": ">=0.1.0 <0.2.0",
54
54
  "@helyx/database": ">=0.8.0 <0.9.0",
55
55
  "@helyx/framework": ">=0.1.0 <0.2.0",
56
- "@helyx/module-manifest": ">=0.6.0 <0.7.0",
57
- "@helyx/platform": ">=0.5.0 <0.6.0",
58
- "@helyx/sdk": ">=0.7.0 <0.8.0",
56
+ "@helyx/module-manifest": ">=0.7.0 <0.8.0",
57
+ "@helyx/platform": ">=0.6.0 <0.7.0",
58
+ "@helyx/sdk": ">=0.8.0 <0.9.0",
59
+ "@helyx/status-client": ">=0.1.0 <0.2.0",
59
60
  "@helyx/verification-contracts": ">=0.3.0 <0.4.0",
60
61
  "discord.js": "14.27.0",
61
62
  "fastify": "5.10.0",