@seedcord/gateway 0.7.5 → 0.8.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.
package/dist/index.mjs CHANGED
@@ -1,23 +1,21 @@
1
1
  import "reflect-metadata";
2
- import { WinstonConsoleSink, WinstonFileSink, installNodeDefaults } from "@seedcord/logger/node";
3
- import { EventMetadataKey, EventMiddlewareMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, MiddlewareRegistry, MissingPermissions, NoticeCard, PermissionNames, PublishDefault, accessorStore, areRoutes, asError, bindBotColor, busLoggerOf, clearStore, decodeComponentRoute, deferFlags, eventResultFor, guardedAccessor, interactionMiddleware, interactionRoutesOf, isEmojiTuple, outcomeFor, pickNotice, queuedMsFor, reportDispatch, reportedWrite, resultFor, runAfter, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
2
+ import { DRAIN_TASK_TIMEOUT_MS, DRAIN_WINDOW_MS, HealthCheck, InFlight, ShutdownPhase, StartupPhase, setEnvaptBaseDir, shutdownOf } from "@seedcord/core/node/internal";
3
+ import { WinstonConsoleSink, WinstonFileSink, installNodeConsole } from "@seedcord/logger/node";
4
+ import { COMMAND_DEPLOY_TIMEOUT_MS, CommandRegistry, EventMetadataKey, EventMiddlewareMetadataKey, FixedScope, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, MiddlewareRegistry, MissingPermissions, NoticeCard, PermissionNames, PublishDefault, SubscriberLoader, accessorStore, attachmentsOf, bindBotColor, busLoggerOf, clearStore, connectDevChannel, decodeComponentRoute, deferFlags, eventResultFor, guardedAccessor, interactionMiddleware, interactionRoutesOf, isEmojiTuple, logUnknownFault, outcomeFor, pickNotice, queuedMsFor, readBotToken, reportDispatch, reportedWrite, resultFor, runAfter, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
4
5
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
5
- import { SeedcordError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
6
+ import { SeedcordError, SeedcordTypeError, throwSingleOrAggregate } from "@seedcord/errors/internal";
6
7
  import { ArraySourceBase, BaseHandler as BaseHandler$1, BaseReplySender, BuilderComponent, Bus, CursorSourceBase, CustomId, DispatchContext, Fault, InteractionKind, Notice, PaginatorBase, RepliableHandler as RepliableHandler$1, ResolvedEmoji, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, assertPermissions, defineGate } from "@seedcord/core";
7
- import { Logger, LoggerChannelRegistry } from "@seedcord/logger";
8
+ import { Logger } from "@seedcord/logger";
8
9
  import { ButtonStyle, Client, ComponentType, DiscordAPIError, Events, GatewayIntentBits, Guild, GuildMember, IntentsBitField, InteractionContextType, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, User } from "discord.js";
9
10
  import { ButtonBuilder, TextDisplayBuilder } from "@discordjs/builders";
10
- import { buildSlashRoute } from "@seedcord/utils/internal";
11
+ import { asError, buildSlashRoute, isStringArray } from "@seedcord/utils/internal";
11
12
  import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
12
- import { CoordinatedShutdown, CoordinatedStartup, Pluggable } from "@seedcord/core/node";
13
- import { CommandRegistry, DRAIN_TASK_TIMEOUT_MS, DRAIN_WINDOW_MS, HealthCheck, ShutdownPhase, StartupPhase, SubscriberLoader, drainInFlight, shutdownOf } from "@seedcord/core/node/internal";
13
+ import { CoordinatedShutdown, CoordinatedStartup, ServerHost } from "@seedcord/core/node";
14
14
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
15
15
  import { HostAugmentTarget, HostVersion, SeedcordBrand } from "@seedcord/types/internal";
16
16
  import { Envapter } from "envapt";
17
- import { Envapt } from "envapt/legacy";
18
- import { HmrModuleHandler } from "@seedcord/core/hmr";
19
- import { formatFilePath, hasKeys } from "@seedcord/utils";
20
- import { traverseDirectory } from "@seedcord/utils/node";
17
+ import { HmrModuleHandler, HmrModuleHandler as HmrModuleHandler$1 } from "@seedcord/core/hmr";
18
+ import { formatFilePath, hasKeys, importFiles } from "@seedcord/utils";
21
19
  import * as crypto from "node:crypto";
22
20
  import { prefixOf } from "@seedcord/custom-id";
23
21
 
@@ -33,8 +31,12 @@ export * from "@seedcord/utils"
33
31
 
34
32
  export * from "@seedcord/event-emitter"
35
33
 
34
+ //#region src/env-bootstrap.ts
35
+ setEnvaptBaseDir();
36
+
37
+ //#endregion
36
38
  //#region src/logger-bootstrap.ts
37
- installNodeDefaults();
39
+ installNodeConsole();
38
40
 
39
41
  //#endregion
40
42
  //#region src/bot/decorators/Events.ts
@@ -147,13 +149,13 @@ function Gated(...gates) {
147
149
  * }
148
150
  * ```
149
151
  *
150
- * @throws A **SeedcordTypeError** If `priority` is not a finite number.
152
+ * @throws A **SeedcordTypeError** when `priority` is not a finite number.
151
153
  */
152
154
  function RegisterEventMiddleware(options = {}) {
153
155
  return function(ctor) {
154
156
  const priority = Number(options.priority ?? 0);
155
- if (!Number.isFinite(priority)) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorInvalidMiddlewarePriority);
156
- if (options.events?.length === 0) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorEmptyMiddlewareFilter, ["events"]);
157
+ if (!Number.isFinite(priority)) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorInvalidMiddlewarePriority, [ctor.name, String(options.priority)]);
158
+ if (options.events?.length === 0) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorEmptyMiddlewareFilter, [ctor.name, "events"]);
157
159
  const metadata = {
158
160
  priority,
159
161
  ...options.events && { events: options.events }
@@ -271,7 +273,8 @@ var GatewayEmoji = class extends ResolvedEmoji {
271
273
  const emojiStorage = accessorStore();
272
274
  /**
273
275
  * The bot's resolved emojis, keyed by {@link EmojiMap}. Filled by {@link EmojiInjector} during startup.
274
- * Throws if you read a key the injector did not fill.
276
+ *
277
+ * @throws A **SeedcordError** when you read a key the injector did not fill.
275
278
  */
276
279
  const Emojis = guardedAccessor("Emojis", emojiStorage);
277
280
  var EmojiInjector = class {
@@ -288,19 +291,25 @@ var EmojiInjector = class {
288
291
  }
289
292
  const configEmojis = this.core.config.bot.emojis;
290
293
  await this.core.bot.client.application?.emojis.fetch();
291
- const failures = [];
294
+ const problems = [];
292
295
  const fetchedGuilds = /* @__PURE__ */ new Set();
293
- for (const [key, value] of Object.entries(configEmojis)) if (isEmojiTuple(value)) await this.resolveTuple(key, value, failures, fetchedGuilds);
294
- else if (typeof value === "string") this.resolveString(key, value, failures);
295
- else failures.push(` - "${key}" has an invalid value (expected a name or [name, guildId])`);
296
- if (failures.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigEmojiUnresolved, [failures.length, failures.join("\n")]);
296
+ for (const [key, value] of Object.entries(configEmojis)) if (isEmojiTuple(value)) await this.resolveTuple(key, value, problems, fetchedGuilds);
297
+ else if (typeof value === "string") this.resolveString(key, value, problems);
298
+ else problems.push(new SeedcordTypeError(SeedcordErrorCode.ConfigEmojiInvalid, [key]));
299
+ throwSingleOrAggregate(problems, SeedcordErrorCode.ConfigEmojiUnresolved);
297
300
  this.logger.utils.summary("Loaded emojis", { emojis: Object.keys(emojiStorage).length });
298
301
  }
299
- async resolveTuple(key, value, failures, fetchedGuilds) {
302
+ async resolveTuple(key, value, problems, fetchedGuilds) {
300
303
  const [emojiName, guildId] = value;
301
304
  const guild = this.core.bot.client.guilds.cache.get(guildId);
302
305
  if (!guild) {
303
- failures.push(` - "${emojiName}" for "${key}" targets guild ${guildId}, which is not available (check the Guilds intent)`);
306
+ problems.push(new SeedcordError(SeedcordErrorCode.ConfigEmojiSourceUnreadable, [
307
+ key,
308
+ emojiName,
309
+ `guild ${guildId}`,
310
+ "discord.js has no cached guild with that id",
311
+ "Check that the bot is in that guild, and that clientOptions.intents includes GatewayIntentBits.Guilds."
312
+ ]));
304
313
  return;
305
314
  }
306
315
  if (!fetchedGuilds.has(guildId)) {
@@ -309,15 +318,23 @@ var EmojiInjector = class {
309
318
  }
310
319
  const guildEmoji = guild.emojis.cache.find((e) => e.name === emojiName);
311
320
  if (!guildEmoji) {
312
- failures.push(` - "${emojiName}" for "${key}" was not found in guild ${guildId}`);
321
+ problems.push(new SeedcordError(SeedcordErrorCode.ConfigEmojiNotFound, [
322
+ key,
323
+ emojiName,
324
+ `guild ${guildId}`
325
+ ]));
313
326
  return;
314
327
  }
315
328
  emojiStorage[key] = new GatewayEmoji(guildEmoji);
316
329
  }
317
- resolveString(key, emojiName, failures) {
330
+ resolveString(key, emojiName, problems) {
318
331
  const appEmoji = this.core.bot.client.application?.emojis.cache.find((e) => e.name === emojiName);
319
332
  if (!appEmoji) {
320
- failures.push(` - "${emojiName}" for "${key}" was not found among the application emojis`);
333
+ problems.push(new SeedcordError(SeedcordErrorCode.ConfigEmojiNotFound, [
334
+ key,
335
+ emojiName,
336
+ "the application"
337
+ ]));
321
338
  return;
322
339
  }
323
340
  emojiStorage[key] = new GatewayEmoji(appEmoji);
@@ -400,7 +417,8 @@ function botRoleOf(guild) {
400
417
  * Validates that the bot can assign a target role. Refuses when the target role is above the bot's, is managed, or the bot lacks Manage Roles.
401
418
  *
402
419
  * @param roleOrOptions - Target role or complete options for the check
403
- * @throws A `Notice` on refusal, or a `SeedcordError` when the bot has no role in the guild's role cache.
420
+ * @throws A {@link Notice} when the check refuses.
421
+ * @throws A **SeedcordError** when the bot has no role in the guild's role cache.
404
422
  *
405
423
  * @example
406
424
  * ```ts
@@ -763,16 +781,24 @@ var SlashHandler = class extends InteractionHandler {
763
781
  async match(arms) {
764
782
  const route = slashRouteOf(this.event);
765
783
  const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
766
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
784
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
785
+ this.constructor.name,
786
+ "command route",
787
+ route
788
+ ]);
767
789
  return await arm(this.options);
768
790
  }
769
791
  };
770
792
 
771
793
  //#endregion
772
794
  //#region src/handlers/interaction/ContextMenuHandler.ts
773
- function armFor(arms, name) {
795
+ function armFor(handler, arms, name) {
774
796
  const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
775
- if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.ContextMenuMatchArmMissing, [name]);
797
+ if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
798
+ handler.constructor.name,
799
+ "context menu command",
800
+ name
801
+ ]);
776
802
  return arm;
777
803
  }
778
804
  /**
@@ -832,7 +858,7 @@ var UserContextMenuHandler = class extends InteractionHandler {
832
858
  * ```
833
859
  */
834
860
  async match(arms) {
835
- return await armFor(arms, this.event.commandName)(this.target, this.targetMember);
861
+ return await armFor(this, arms, this.event.commandName)(this.target, this.targetMember);
836
862
  }
837
863
  };
838
864
  /**
@@ -887,7 +913,7 @@ var MessageContextMenuHandler = class extends InteractionHandler {
887
913
  * ```
888
914
  */
889
915
  async match(arms) {
890
- return await armFor(arms, this.event.commandName)(this.target);
916
+ return await armFor(this, arms, this.event.commandName)(this.target);
891
917
  }
892
918
  };
893
919
 
@@ -952,11 +978,11 @@ var AutocompleteHandler = class extends BaseHandler {
952
978
  *
953
979
  * Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
954
980
  * arm receives the focused partial value and a `respond` typed to that field's choice value. The
955
- * arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
956
- * focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
981
+ * arms must cover every autocompletable field. A missing field or an unknown key is a compile error.
957
982
  *
958
983
  * @param arms - One handler per autocompletable field, keyed by field name.
959
984
  * @returns The result of the arm that ran.
985
+ * @throws A **SeedcordTypeError** with `MatchArmMissing` when the focused field has no arm. Only a stale-deployed command can send one.
960
986
  *
961
987
  * @example
962
988
  * ```ts
@@ -975,7 +1001,11 @@ var AutocompleteHandler = class extends BaseHandler {
975
1001
  const { name, value } = this.focused;
976
1002
  const respond = (choices) => this.respond(choices);
977
1003
  const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
978
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
1004
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
1005
+ this.constructor.name,
1006
+ "focused field",
1007
+ name
1008
+ ]);
979
1009
  return await arm(value, respond);
980
1010
  }
981
1011
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
@@ -1028,10 +1058,11 @@ var ComponentHandler = class extends InteractionHandler {
1028
1058
  /**
1029
1059
  * The decoded params of the single route this handler is registered for.
1030
1060
  *
1031
- * Reading this decodes `this.event.customId` once (cached after the first read) and throws
1032
- * `StaleCustomId` or `InvalidCustomId` when the wire no longer matches the current shape, which the
1033
- * controller boundary turns into a reply. On a handler registered for several routes this is
1034
- * `never`. Use {@link match} instead.
1061
+ * Reading this decodes `this.event.customId` once and caches the result. On a handler registered
1062
+ * for several routes this is `never`. Use {@link match} instead.
1063
+ *
1064
+ * @throws A **StaleCustomId** when the wire was encoded under an older shape. The controller boundary turns it into a reply.
1065
+ * @throws An **InvalidCustomId** when the wire is corrupt. The controller boundary turns it into a reply.
1035
1066
  */
1036
1067
  get params() {
1037
1068
  return this.route.params;
@@ -1042,12 +1073,14 @@ var ComponentHandler = class extends InteractionHandler {
1042
1073
  * handler `this.params` is `never`, leaving match as the only way to read the decoded params.
1043
1074
  *
1044
1075
  * Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
1045
- * decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
1046
- * unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
1047
- * corrupt wire throws before an arm body executes.
1076
+ * decoded params. The compiler checks that the arms cover every registered route prefix. `match`
1077
+ * decodes the wire before it runs an arm.
1048
1078
  *
1049
1079
  * @param arms - One callback per registered route, keyed by prefix.
1050
1080
  * @returns The result of the arm that ran.
1081
+ * @throws A **SeedcordTypeError** with `MatchArmMissing` when the wire's prefix has no arm.
1082
+ * @throws A **StaleCustomId** when the wire was encoded under an older shape.
1083
+ * @throws An **InvalidCustomId** when the wire is corrupt.
1051
1084
  *
1052
1085
  * @example
1053
1086
  * ```ts
@@ -1065,7 +1098,11 @@ var ComponentHandler = class extends InteractionHandler {
1065
1098
  async match(arms) {
1066
1099
  const { prefix, params } = this.route;
1067
1100
  const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1068
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1101
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
1102
+ this.constructor.name,
1103
+ "component route",
1104
+ prefix
1105
+ ]);
1069
1106
  return await arm(params);
1070
1107
  }
1071
1108
  };
@@ -1353,9 +1390,13 @@ var EventHandler = class extends BaseHandler {
1353
1390
  */
1354
1391
  async match(arms) {
1355
1392
  const name = this.firedEvent;
1356
- if (name === void 0) throw new SeedcordTypeError(SeedcordErrorCode.EventMatchArmMissing, ["<no event>"]);
1393
+ if (name === void 0) throw new SeedcordError(SeedcordErrorCode.EventNameUnavailable, [this.constructor.name, "match()"]);
1357
1394
  const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1358
- if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.EventMatchArmMissing, [name]);
1395
+ if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
1396
+ this.constructor.name,
1397
+ "event",
1398
+ name
1399
+ ]);
1359
1400
  return await arm(...this.getEvent());
1360
1401
  }
1361
1402
  };
@@ -1385,7 +1426,7 @@ var EventMiddleware = class extends BaseHandler {
1385
1426
  * `this.event` directly and does not need this.
1386
1427
  */
1387
1428
  get eventName() {
1388
- if (this.firedEvent === void 0) throw new SeedcordError(SeedcordErrorCode.EventMiddlewareNameUnavailable);
1429
+ if (this.firedEvent === void 0) throw new SeedcordError(SeedcordErrorCode.EventNameUnavailable, [this.constructor.name, "this.eventName"]);
1389
1430
  return this.firedEvent;
1390
1431
  }
1391
1432
  };
@@ -1492,6 +1533,10 @@ var WebhookLog = class extends WebhookLog$1 {};
1492
1533
 
1493
1534
  //#endregion
1494
1535
  //#region src/Plugin.ts
1536
+ const GATEWAY_SCOPE = {
1537
+ transport: "gateway",
1538
+ runtime: "server"
1539
+ };
1495
1540
  /**
1496
1541
  * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
1497
1542
  *
@@ -1500,7 +1545,10 @@ var WebhookLog = class extends WebhookLog$1 {};
1500
1545
  *
1501
1546
  * @typeParam Opts - The transport and runtime the plugin declares.
1502
1547
  */
1503
- var Plugin = class extends Plugin$1 {};
1548
+ var Plugin = class extends Plugin$1 {
1549
+ /** @internal */
1550
+ static [FixedScope] = GATEWAY_SCOPE;
1551
+ };
1504
1552
 
1505
1553
  //#endregion
1506
1554
  //#region src/miscellaneous/deriveEventActor.ts
@@ -1635,15 +1683,8 @@ function reportFault(denial, core, fault, uuid) {
1635
1683
  metadata: metadataFor(fault)
1636
1684
  });
1637
1685
  }
1638
- function causeLine(error) {
1639
- const { cause } = error;
1640
- if (!Error.isError(cause)) return "";
1641
- const [first] = cause.message.split("\n");
1642
- return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
1643
- }
1644
1686
  function reportRawFault(error, core, fault, uuid) {
1645
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
1646
- else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
1687
+ logUnknownFault(logger$2, uuid, error, core.config.errors);
1647
1688
  core.bus[PublishDefault]("unknownException", {
1648
1689
  uuid,
1649
1690
  dispatchId: fault.dispatch.id,
@@ -1783,8 +1824,7 @@ var EventDispatcher = class {
1783
1824
  middlewares = new MiddlewareRegistry(eventMiddleware);
1784
1825
  executedOnceHandlers = /* @__PURE__ */ new Set();
1785
1826
  attachedEvents = /* @__PURE__ */ new Set();
1786
- inFlight = /* @__PURE__ */ new Set();
1787
- draining = false;
1827
+ inFlight = new InFlight(this.logger, "Events");
1788
1828
  loading = false;
1789
1829
  loadedHandlers = [];
1790
1830
  loadedMiddlewares = [];
@@ -1794,7 +1834,7 @@ var EventDispatcher = class {
1794
1834
  const eventsDir = this.core.config.bot.events.path;
1795
1835
  if (!eventsDir) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["EventDispatcher", "events"]);
1796
1836
  if (!Envapter.isDevelopment && !Envapter.isTest) return;
1797
- this.hmrHandler = new HmrModuleHandler({
1837
+ this.hmrHandler = new HmrModuleHandler$1({
1798
1838
  handlersDir: eventsDir,
1799
1839
  ...hasKeys(this.core.config.bot.events, ["middlewares"]) && { middlewaresDir: this.core.config.bot.events.middlewares },
1800
1840
  isHandler: this.isEventHandlerClass.bind(this),
@@ -1842,22 +1882,18 @@ var EventDispatcher = class {
1842
1882
  utils.block("Loaded event handlers", [...utils.entries(this.loadedHandlers), ...utils.counts(perEvent)], "debug");
1843
1883
  }
1844
1884
  async loadHandlers(dir) {
1845
- await traverseDirectory(dir, (fullPath, relativePath, imported) => {
1846
- for (const val of Object.values(imported)) {
1847
- if (!this.isEventHandlerClass(val)) continue;
1848
- this.registerHandler(val, relativePath);
1849
- this.hmrHandler?.trackHandler(fullPath, val);
1850
- }
1851
- });
1885
+ for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const val of Object.values(imported)) {
1886
+ if (!this.isEventHandlerClass(val)) continue;
1887
+ this.registerHandler(val, relativePath);
1888
+ this.hmrHandler?.trackHandler(fullPath, val);
1889
+ }
1852
1890
  }
1853
1891
  async loadMiddlewares(dir) {
1854
- await traverseDirectory(dir, (fullPath, relativePath, imported) => {
1855
- for (const val of Object.values(imported)) {
1856
- if (!this.isMiddlewareClass(val)) continue;
1857
- this.registerMiddleware(val, relativePath);
1858
- this.hmrHandler?.trackMiddleware(fullPath, val);
1859
- }
1860
- });
1892
+ for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const val of Object.values(imported)) {
1893
+ if (!this.isMiddlewareClass(val)) continue;
1894
+ this.registerMiddleware(val, relativePath);
1895
+ this.hmrHandler?.trackMiddleware(fullPath, val);
1896
+ }
1861
1897
  }
1862
1898
  /** @internal */
1863
1899
  async onHmr(event) {
@@ -1926,7 +1962,7 @@ var EventDispatcher = class {
1926
1962
  };
1927
1963
  if (Array.isArray(raw)) for (const entry of raw) register(entry.event, entry.frequency);
1928
1964
  else {
1929
- const names = areRoutes(raw) ? raw : typeof raw === "string" ? [raw] : [];
1965
+ const names = isStringArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
1930
1966
  for (const name of names) register(name, "on");
1931
1967
  }
1932
1968
  const from = formatFilePath(relativePath);
@@ -1944,21 +1980,20 @@ var EventDispatcher = class {
1944
1980
  const handlerEntries = this.eventMap.get(eventName);
1945
1981
  this.logger.debug(`Attaching ${paint.sky.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
1946
1982
  this.core.bot.client.on(eventName, (...args) => {
1947
- if (this.draining) return;
1983
+ if (this.inFlight.closed) return;
1948
1984
  const run = this.processEvent(eventName, args).catch((caught) => {
1949
1985
  const error = asError(caught);
1950
1986
  this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
1951
1987
  this.core.bus[PublishDefault]("unhandledEventError", { error });
1952
1988
  });
1953
- this.inFlight.add(run);
1954
- run.finally(() => this.inFlight.delete(run));
1989
+ this.inFlight.track(run);
1955
1990
  });
1956
1991
  }
1957
1992
  stopAccepting() {
1958
- this.draining = true;
1993
+ this.inFlight.close();
1959
1994
  }
1960
1995
  drain(timeoutMs) {
1961
- return drainInFlight(this.inFlight, timeoutMs, this.logger, "Events");
1996
+ return this.inFlight.drain(timeoutMs);
1962
1997
  }
1963
1998
  async processEvent(eventName, args) {
1964
1999
  const startedAt = performance.now();
@@ -2125,8 +2160,7 @@ var InteractionDispatcher = class {
2125
2160
  handlerFiles = /* @__PURE__ */ new Map();
2126
2161
  keysToIgnore = /* @__PURE__ */ new Set();
2127
2162
  middlewares = new MiddlewareRegistry(interactionMiddleware);
2128
- inFlight = /* @__PURE__ */ new Set();
2129
- draining = false;
2163
+ inFlight = new InFlight(this.logger, "Interactions");
2130
2164
  loading = false;
2131
2165
  loadedHandlers = [];
2132
2166
  loadedMiddlewares = [];
@@ -2139,7 +2173,7 @@ var InteractionDispatcher = class {
2139
2173
  const interactionsDir = this.core.config.bot.interactions.path;
2140
2174
  if (!interactionsDir) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["InteractionDispatcher", "interactions"]);
2141
2175
  if (!Envapter.isDevelopment && !Envapter.isTest) return;
2142
- this.hmrHandler = new HmrModuleHandler({
2176
+ this.hmrHandler = new HmrModuleHandler$1({
2143
2177
  handlersDir: interactionsDir,
2144
2178
  ...hasKeys(this.core.config.bot.interactions, ["middlewares"]) && this.core.config.bot.interactions.middlewares && { middlewaresDir: this.core.config.bot.interactions.middlewares },
2145
2179
  isHandler: this.isHandlerClass.bind(this),
@@ -2205,22 +2239,18 @@ var InteractionDispatcher = class {
2205
2239
  })], "debug");
2206
2240
  }
2207
2241
  async loadHandlers(dir) {
2208
- await traverseDirectory(dir, (fullPath, relativePath, imported) => {
2209
- for (const val of Object.values(imported)) {
2210
- if (!this.isHandlerClass(val)) continue;
2211
- this.registerHandler(val, relativePath);
2212
- this.hmrHandler?.trackHandler(fullPath, val);
2213
- }
2214
- });
2242
+ for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const val of Object.values(imported)) {
2243
+ if (!this.isHandlerClass(val)) continue;
2244
+ this.registerHandler(val, relativePath);
2245
+ this.hmrHandler?.trackHandler(fullPath, val);
2246
+ }
2215
2247
  }
2216
2248
  async loadMiddlewares(dir) {
2217
- await traverseDirectory(dir, (fullPath, relativePath, imported) => {
2218
- for (const val of Object.values(imported)) {
2219
- if (!this.isMiddlewareClass(val)) continue;
2220
- this.registerMiddleware(val, relativePath);
2221
- this.hmrHandler?.trackMiddleware(fullPath, val);
2222
- }
2223
- });
2249
+ for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const val of Object.values(imported)) {
2250
+ if (!this.isMiddlewareClass(val)) continue;
2251
+ this.registerMiddleware(val, relativePath);
2252
+ this.hmrHandler?.trackMiddleware(fullPath, val);
2253
+ }
2224
2254
  }
2225
2255
  registerMiddleware(middlewareCtor, relativePath) {
2226
2256
  const metadata = this.middlewares.register(middlewareCtor);
@@ -2244,10 +2274,10 @@ var InteractionDispatcher = class {
2244
2274
  const writes = [];
2245
2275
  for (const [kind, routes] of interactionRoutesOf(handlerClass)) for (const route of routes) {
2246
2276
  const existing = this.maps[kind].get(route);
2247
- if (existing && existing !== handlerClass) throw new SeedcordError(SeedcordErrorCode.InteractionDuplicateRoute, [
2277
+ if (existing && existing !== handlerClass) throw new SeedcordTypeError(SeedcordErrorCode.CoreDuplicateName, [
2278
+ "route",
2248
2279
  `${kind}:${route}`,
2249
- `${existing.name} (${this.handlerFiles.get(existing) ?? "unknown file"})`,
2250
- `${handlerClass.name} (${from})`
2280
+ [`${paint.bold(existing.name)} (${paint.path(this.handlerFiles.get(existing) ?? "unknown file")})`, `${paint.bold(handlerClass.name)} (${paint.path(from)})`]
2251
2281
  ]);
2252
2282
  writes.push({
2253
2283
  kind,
@@ -2274,22 +2304,21 @@ var InteractionDispatcher = class {
2274
2304
  }
2275
2305
  attachToClient() {
2276
2306
  this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
2277
- if (this.draining) return;
2307
+ if (this.inFlight.closed) return;
2278
2308
  this.core.bus[PublishDefault]("anyInteraction", { interaction });
2279
2309
  const run = this.handleInteraction(interaction).catch((caught) => {
2280
2310
  const error = asError(caught);
2281
2311
  this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2282
2312
  this.core.bus[PublishDefault]("unhandledInteractionError", { error });
2283
2313
  });
2284
- this.inFlight.add(run);
2285
- run.finally(() => this.inFlight.delete(run));
2314
+ this.inFlight.track(run);
2286
2315
  });
2287
2316
  }
2288
2317
  stopAccepting() {
2289
- this.draining = true;
2318
+ this.inFlight.close();
2290
2319
  }
2291
2320
  drain(timeoutMs) {
2292
- return drainInFlight(this.inFlight, timeoutMs, this.logger, "Interactions");
2321
+ return this.inFlight.drain(timeoutMs);
2293
2322
  }
2294
2323
  async handleCustomIdInteraction(interaction, kind) {
2295
2324
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
@@ -2487,16 +2516,7 @@ function assertGuildsIntent(intents, commands) {
2487
2516
  if ((IntentsBitField.resolve(intents) & GatewayIntentBits.Guilds) !== 0) return;
2488
2517
  const needing = commands.filter((command) => reachesGuilds(command)).map((command) => command.name);
2489
2518
  if (needing.length === 0) return;
2490
- throw new SeedcordError(SeedcordErrorCode.MissingGuildsIntent, [needing.join(", ")]);
2491
- }
2492
-
2493
- //#endregion
2494
- //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
2495
- function __decorate(decorators, target, key, desc) {
2496
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2497
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2498
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2499
- return c > 3 && r && Object.defineProperty(target, key, r), r;
2519
+ throw new SeedcordError(SeedcordErrorCode.MissingGuildsIntent, [needing]);
2500
2520
  }
2501
2521
 
2502
2522
  //#endregion
@@ -2504,16 +2524,19 @@ function __decorate(decorators, target, key, desc) {
2504
2524
  const UNBIND_TIMEOUT_MS = 2e3;
2505
2525
  const LOGOUT_TIMEOUT_MS = 2e3;
2506
2526
  const loggerSlot = Symbol("seedcord:bot:logger");
2507
- const initSlot = Symbol("seedcord:bot:init");
2508
2527
  const controllersSlot = Symbol("seedcord:bot:controllers");
2509
2528
  /**
2510
2529
  * The Discord client and its controllers. Access it through `core.bot`.
2511
2530
  */
2512
2531
  var Bot = class {
2532
+ get botToken() {
2533
+ return readBotToken().value;
2534
+ }
2513
2535
  #logger = new Logger("Bot", { channel: "bot" });
2514
2536
  /** @internal */
2515
2537
  [loggerSlot] = this.#logger;
2516
2538
  #isInitialized = false;
2539
+ #readyToDeploy = Promise.withResolvers();
2517
2540
  #intents;
2518
2541
  #client;
2519
2542
  #emojiInjector;
@@ -2534,6 +2557,7 @@ var Bot = class {
2534
2557
  if (core.config.bot.events.path) this.#events = new EventDispatcher(core);
2535
2558
  if (core.config.bot.commands.path) this.#commandRegistry = new CommandRegistry(core);
2536
2559
  this.#emojiInjector = new EmojiInjector(core);
2560
+ this.#registerStartupTasks(core);
2537
2561
  this.#registerShutdownTasks(core);
2538
2562
  }
2539
2563
  /** @internal */
@@ -2550,6 +2574,16 @@ var Bot = class {
2550
2574
  if (!application) throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable);
2551
2575
  return application.id;
2552
2576
  }
2577
+ #registerStartupTasks(core) {
2578
+ core.startup.addTask(StartupPhase.Login, "bot-initialization", async (signal) => {
2579
+ this.#logger.utils.initialization("Bot", "start");
2580
+ await this.#init(signal);
2581
+ this.#logger.utils.initialization("Bot", "end");
2582
+ });
2583
+ const registry = this.#commandRegistry;
2584
+ if (!registry) return;
2585
+ core.startup.addTask(StartupPhase.Login, "command-deploy", (signal) => this.#deployCommands(registry, signal), COMMAND_DEPLOY_TIMEOUT_MS);
2586
+ }
2553
2587
  #registerShutdownTasks(core) {
2554
2588
  core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
2555
2589
  this.#stopAccepting();
@@ -2565,21 +2599,28 @@ var Bot = class {
2565
2599
  async #drain() {
2566
2600
  await Promise.allSettled([this.#interactions?.drain(DRAIN_WINDOW_MS), this.#events?.drain(DRAIN_WINDOW_MS)]);
2567
2601
  }
2568
- /** @internal */
2569
- async [initSlot]() {
2602
+ async #init(signal) {
2570
2603
  if (this.#isInitialized) return;
2571
2604
  this.#isInitialized = true;
2572
- const token = this.botToken;
2573
- if (this.#interactions) await this.#interactions.init();
2574
- if (this.#events) await this.#events.init();
2575
- await this.#login(token);
2576
- await this.#emojiInjector.init();
2577
- if (!this.#commandRegistry) return;
2578
- await this.#commandRegistry.init();
2579
- assertGuildsIntent(this.#intents, this.#commandRegistry.allCommands());
2580
- await this.#commandRegistry.setCommands();
2581
- this.#interactions?.warnUnhandledRoutes(this.#commandRegistry.routeLeaves());
2582
- this.#interactions?.warnUnhandledContextMenuRoutes(this.#commandRegistry.contextMenuLeaves());
2605
+ signal.addEventListener("abort", () => this.#readyToDeploy.resolve(false), { once: true });
2606
+ try {
2607
+ const token = this.botToken;
2608
+ if (this.#interactions) await this.#interactions.init();
2609
+ if (this.#events) await this.#events.init();
2610
+ await this.#login(token);
2611
+ await this.#emojiInjector.init();
2612
+ } catch (error) {
2613
+ this.#readyToDeploy.resolve(false);
2614
+ throw error;
2615
+ }
2616
+ this.#readyToDeploy.resolve(true);
2617
+ }
2618
+ async #deployCommands(registry, signal) {
2619
+ if (!await this.#readyToDeploy.promise) return;
2620
+ await registry.deploy(signal, {
2621
+ routes: this.#interactions,
2622
+ check: (commands) => assertGuildsIntent(this.#intents, commands)
2623
+ });
2583
2624
  }
2584
2625
  async #stop() {
2585
2626
  this.#client.removeAllListeners();
@@ -2601,18 +2642,11 @@ var Bot = class {
2601
2642
  return this.#client;
2602
2643
  }
2603
2644
  };
2604
- __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) })], Bot.prototype, "botToken", void 0);
2605
- function botLoggerOf(bot) {
2606
- return bot[loggerSlot];
2607
- }
2608
- function initBot(bot) {
2609
- return bot[initSlot]();
2610
- }
2611
2645
 
2612
2646
  //#endregion
2613
2647
  //#region src/version.ts
2614
2648
  /** Package version */
2615
- const version = "0.7.5";
2649
+ const version = "0.8.0";
2616
2650
 
2617
2651
  //#endregion
2618
2652
  //#region src/Seedcord.ts
@@ -2620,7 +2654,7 @@ const version = "0.7.5";
2620
2654
  * The gateway bot host. Opens a discord.js gateway session, discovers handlers, and runs
2621
2655
  * coordinated startup and shutdown. Attach plugins with `attach()`.
2622
2656
  */
2623
- var Seedcord = class Seedcord extends Pluggable {
2657
+ var Seedcord = class extends ServerHost {
2624
2658
  config;
2625
2659
  /** @internal */
2626
2660
  [SeedcordBrand] = true;
@@ -2628,11 +2662,11 @@ var Seedcord = class Seedcord extends Pluggable {
2628
2662
  [HostAugmentTarget] = "@seedcord/gateway";
2629
2663
  /** @internal */
2630
2664
  [HostVersion] = version;
2631
- healthCheck;
2632
- hmrManager;
2665
+ #healthCheck;
2666
+ #hmrManager;
2633
2667
  /** @see {@link Bus} */
2634
2668
  bus;
2635
- subscribers;
2669
+ #subscribers;
2636
2670
  /** @see {@link Bot} */
2637
2671
  bot;
2638
2672
  /** The discord.js client's REST client. Its token is set during the Login phase. */
@@ -2645,76 +2679,61 @@ var Seedcord = class Seedcord extends Pluggable {
2645
2679
  rateLimiter;
2646
2680
  /**
2647
2681
  * @param config - Bot configuration including paths and Discord client options
2648
- * @throws A **SeedcordError** When attempting to create multiple instances (singleton)
2682
+ * @throws A **SeedcordError** when another `Seedcord` exists in this process and has not shut down.
2683
+ * @throws A **SeedcordError** when the running Node or Bun is older than the range in `engines`.
2649
2684
  */
2650
2685
  constructor(config) {
2651
- super(new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup());
2686
+ super("gateway", new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup());
2652
2687
  this.config = config;
2653
- installNodeDefaults(config.logger);
2654
2688
  bindBotColor(() => this.config.botColor);
2655
- this.hmrManager = new HmrManager();
2656
- this.hmrManager.init();
2689
+ this.#hmrManager = new HmrManager();
2690
+ this.#hmrManager.init(connectDevChannel());
2657
2691
  this.bus = new Bus(this);
2658
- this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
2692
+ this.#subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
2659
2693
  this.bot = new Bot(this);
2660
2694
  this.rest = this.bot.client.rest;
2661
2695
  this.rateLimiter = config.store ?? new MemoryRateLimiter();
2662
- this.healthCheck = HealthCheck.fromOption(shutdownOf(this), config.healthCheck);
2663
- this.registerStartupTasks();
2696
+ this.#healthCheck = HealthCheck.fromOption(shutdownOf(this), config.healthCheck);
2697
+ this.#registerStartupTasks();
2664
2698
  }
2665
2699
  /** The bot's discord username, populated after login. */
2666
2700
  get username() {
2667
2701
  return this.bot.client.user?.username;
2668
2702
  }
2669
- static reset(host) {
2670
- if (!super.reset(host)) return false;
2671
- LoggerChannelRegistry.instance.configure({});
2672
- return true;
2673
- }
2674
- registerStartupTasks() {
2675
- if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
2703
+ #registerStartupTasks() {
2704
+ if (Envapter.isDevelopment || Envapter.isTest) this.#registerHmrAwareModules();
2676
2705
  this.startup.addTask(StartupPhase.Configuration, "bus-initialization", async () => {
2677
2706
  busLoggerOf(this.bus).utils.initialization("Subscribers", "start");
2678
- await this.subscribers.init();
2707
+ await this.#subscribers.init();
2679
2708
  busLoggerOf(this.bus).utils.initialization("Subscribers", "end");
2680
2709
  });
2681
- this.startup.addTask(StartupPhase.Login, "bot-initialization", async () => {
2682
- botLoggerOf(this.bot).utils.initialization("Bot", "start");
2683
- await initBot(this.bot);
2684
- botLoggerOf(this.bot).utils.initialization("Bot", "end");
2685
- });
2686
- const { healthCheck } = this;
2687
- if (healthCheck) this.startup.addTask(StartupPhase.Ready, "health-check", async () => {
2688
- healthCheck.logger.utils.initialization("HealthCheck", "start");
2689
- await healthCheck.init();
2690
- healthCheck.logger.utils.initialization("HealthCheck", "end");
2691
- });
2710
+ }
2711
+ /** @internal */
2712
+ async openHealth() {
2713
+ const healthCheck = this.#healthCheck;
2714
+ if (!healthCheck) return;
2715
+ healthCheck.logger.utils.initialization("HealthCheck", "start");
2716
+ await healthCheck.init();
2717
+ healthCheck.logger.utils.initialization("HealthCheck", "end");
2692
2718
  }
2693
2719
  /**
2694
2720
  * Starts the bot and runs all initialization tasks
2695
2721
  *
2696
2722
  * @returns This Seedcord instance when initialized
2697
2723
  */
2698
- async start() {
2699
- try {
2700
- await super.init();
2701
- } catch (caught) {
2702
- await shutdownOf(this).run(1, false);
2703
- Seedcord.reset(this);
2704
- throw caught;
2705
- }
2706
- return this;
2724
+ start() {
2725
+ return this.init();
2707
2726
  }
2708
- registerHmrAwareModules() {
2727
+ #registerHmrAwareModules() {
2709
2728
  this.startup.addTask(StartupPhase.Configuration, "hmr-registration", async () => {
2710
- this.hmrManager.register(this.bot);
2711
- this.hmrManager.register(this.subscribers);
2712
- for (const plugin of this.plugins) this.hmrManager.register(plugin);
2729
+ this.#hmrManager.register(this.bot);
2730
+ this.#hmrManager.register(this.#subscribers);
2731
+ for (const { instance } of attachmentsOf(this)) this.#hmrManager.register(instance);
2713
2732
  await Promise.resolve();
2714
2733
  });
2715
2734
  }
2716
2735
  };
2717
2736
 
2718
2737
  //#endregion
2719
- export { ArraySource, AutocompleteHandler, BaseHandler, ButtonHandler, ChannelMenuHandler, ComponentHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, GatewayEmoji, IgnoreBots, InteractionHandler, InteractionMiddleware, MentionableMenuHandler, MessageContextMenuHandler, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RegisterEventMiddleware, RepliableHandler, ReplySender, RoleMenuHandler, Seedcord, SelectMenuHandler, SlashHandler, StringMenuHandler, Subscriber, UserContextMenuHandler, UserMenuHandler, WebhookLog, WinstonConsoleSink, WinstonFileSink, checkBotPermissions, checkPermissions, getConfirmation, hasPermsToAssign, version };
2738
+ export { ArraySource, AutocompleteHandler, BaseHandler, ButtonHandler, ChannelMenuHandler, ComponentHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, GatewayEmoji, HmrModuleHandler, IgnoreBots, InteractionHandler, InteractionMiddleware, MentionableMenuHandler, MessageContextMenuHandler, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RegisterEventMiddleware, RepliableHandler, ReplySender, RoleMenuHandler, Seedcord, SelectMenuHandler, SlashHandler, StringMenuHandler, Subscriber, UserContextMenuHandler, UserMenuHandler, WebhookLog, WinstonConsoleSink, WinstonFileSink, checkBotPermissions, checkPermissions, getConfirmation, hasPermsToAssign, version };
2720
2739
  //# sourceMappingURL=index.mjs.map