@seedcord/gateway 0.1.0-next.3 → 0.1.0-next.5

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.cjs CHANGED
@@ -38,19 +38,17 @@ let _seedcord_utils_internal = require("@seedcord/utils/internal");
38
38
  let discord_js = require("discord.js");
39
39
  let node_crypto = require("node:crypto");
40
40
  node_crypto = __toESM(node_crypto, 1);
41
- let _seedcord_event_emitter = require("@seedcord/event-emitter");
42
- let chalk = require("chalk");
43
- chalk = __toESM(chalk, 1);
44
- let envapt = require("envapt");
45
- let discord_api_types_v10 = require("discord-api-types/v10");
46
- let _seedcord_utils = require("@seedcord/utils");
47
- let _discordjs_rest = require("@discordjs/rest");
48
- let _seedcord_core_node = require("@seedcord/core/node");
41
+ let _seedcord_core_plugin = require("@seedcord/core/plugin");
42
+ let _seedcord_core_node_internal = require("@seedcord/core/node/internal");
49
43
  let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
50
44
  let _seedcord_types_internal = require("@seedcord/types/internal");
45
+ let envapt = require("envapt");
46
+ let chalk = require("chalk");
47
+ chalk = __toESM(chalk, 1);
51
48
  let envapt_legacy = require("envapt/legacy");
52
49
  let node_path = require("node:path");
53
50
  let _seedcord_core_hmr = require("@seedcord/core/hmr");
51
+ let _seedcord_utils = require("@seedcord/utils");
54
52
  let _seedcord_utils_node = require("@seedcord/utils/node");
55
53
 
56
54
  //#region src/logger-bootstrap.ts
@@ -165,7 +163,7 @@ let MiddlewareType = /* @__PURE__ */ function(MiddlewareType) {
165
163
  * Interaction middleware cannot specify event filters.
166
164
  *
167
165
  * @param type - Middleware kind from {@link MiddlewareType}
168
- * @param priority - Ordering value where lower runs earlier. {@default `0`}
166
+ * @param priority - Ordering value where lower runs earlier. {@default `0` }
169
167
  * @param options - Additional registration options
170
168
  *
171
169
  * @decorator
@@ -353,7 +351,7 @@ const Emojis = emojiStorage;
353
351
  */
354
352
  var EmojiInjector = class {
355
353
  core;
356
- logger = new _seedcord_logger.Logger("Emojis");
354
+ logger = new _seedcord_logger.Logger("Emojis", { channel: "bot" });
357
355
  constructor(core) {
358
356
  this.core = core;
359
357
  }
@@ -428,7 +426,7 @@ const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
428
426
  */
429
427
  var CommandMentionInjector = class {
430
428
  core;
431
- logger = new _seedcord_logger.Logger("CommandMentions");
429
+ logger = new _seedcord_logger.Logger("CommandMentions", { channel: "bot" });
432
430
  constructor(core) {
433
431
  this.core = core;
434
432
  }
@@ -586,7 +584,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
586
584
  }
587
585
  const me = target.guild.members.me;
588
586
  if (!me) {
589
- new _seedcord_logger.Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
587
+ new _seedcord_logger.Logger("checkBotPermissions", { channel: "errors" }).warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
590
588
  const names = scope.map((bit) => _seedcord_core_internal.PermissionNames.get(bit) ?? String(bit));
591
589
  throw new ((errors?.missingNotice) ?? _seedcord_core_internal.MissingPermissions)(void 0, "The bot", names);
592
590
  }
@@ -785,8 +783,11 @@ function seedState(interaction) {
785
783
  */
786
784
  var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
787
785
  interaction;
788
- constructor(interaction, routeId) {
789
- super(routeId, seedState(interaction));
786
+ constructor(interaction, routeId, bus) {
787
+ super(routeId, seedState(interaction), bus && {
788
+ bus,
789
+ interactionId: interaction.id
790
+ });
790
791
  this.interaction = interaction;
791
792
  }
792
793
  async writeReply(response, opts) {
@@ -893,43 +894,9 @@ function slashRouteOf(interaction) {
893
894
  return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
894
895
  }
895
896
 
896
- //#endregion
897
- //#region src/miscellaneous/FaultThrottle.ts
898
- /**
899
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
900
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
901
- * the map stays small, and a stale entry is evicted on the next read of its key.
902
- *
903
- * @internal
904
- */
905
- var FaultThrottle = class {
906
- windowMs;
907
- now;
908
- lastReportedAt = /* @__PURE__ */ new Map();
909
- constructor(windowMs = 6e4, now = () => Date.now()) {
910
- this.windowMs = windowMs;
911
- this.now = now;
912
- }
913
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
914
- shouldReport(key) {
915
- const last = this.lastReportedAt.get(key);
916
- if (last === void 0) return true;
917
- if (this.now() - last < this.windowMs) return false;
918
- this.lastReportedAt.delete(key);
919
- return true;
920
- }
921
- markReported(key) {
922
- this.lastReportedAt.set(key, this.now());
923
- }
924
- clear() {
925
- this.lastReportedAt.clear();
926
- }
927
- };
928
-
929
897
  //#endregion
930
898
  //#region src/miscellaneous/extractErrorResponse.ts
931
- const logger$2 = new _seedcord_logger.Logger("ErrorsHandling");
932
- const faultThrottle = new FaultThrottle();
899
+ const logger$2 = new _seedcord_logger.Logger("ErrorsHandling", { channel: "errors" });
933
900
  /**
934
901
  * Processes an error into a user-facing reply and routes its report.
935
902
  *
@@ -959,29 +926,30 @@ function extractErrorResponse(error, core, origin) {
959
926
  response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
960
927
  };
961
928
  }
962
- function withThrottle(origin, error, publish) {
929
+ function withThrottle(core, origin, error, publish) {
930
+ const throttle = _seedcord_core_internal.FaultThrottle.for(core);
963
931
  const key = faultKey(origin, error);
964
- if (!faultThrottle.shouldReport(key)) {
965
- logger$2.info(`throttled duplicate fault ${key}`);
932
+ if (!throttle.shouldReport(key)) {
933
+ logger$2.debug(`throttled duplicate fault ${key}`);
966
934
  return;
967
935
  }
968
936
  publish();
969
- faultThrottle.markReported(key);
937
+ throttle.markReported(key);
970
938
  }
971
939
  function reportFault(denial, core, origin, uuid) {
972
- withThrottle(origin, denial, () => {
973
- logger$2.error(`${denial.name}: ${uuid}`, denial);
974
- if (origin.interaction) core.bus.publish("handledException", {
940
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
941
+ withThrottle(core, origin, denial, () => {
942
+ if (origin.interaction) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
975
943
  denial,
976
944
  uuid,
977
945
  source: buildInteractionSource(origin.interaction)
978
946
  });
979
- else if (origin.event) core.bus.publish("handledException", {
947
+ else if (origin.event) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
980
948
  denial,
981
949
  uuid,
982
950
  source: buildEventSource(origin.event, origin)
983
951
  });
984
- else core.bus.publish("unknownException", {
952
+ else core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
985
953
  uuid,
986
954
  error: denial,
987
955
  ...scalarActors(origin),
@@ -996,10 +964,10 @@ function causeLine(error) {
996
964
  return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
997
965
  }
998
966
  function reportRawFault(error, core, origin, uuid) {
999
- withThrottle(origin, error, () => {
1000
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
1001
- else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
1002
- core.bus.publish("unknownException", {
967
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
968
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
969
+ withThrottle(core, origin, error, () => {
970
+ core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
1003
971
  uuid,
1004
972
  error,
1005
973
  ...scalarActors(origin),
@@ -1142,112 +1110,6 @@ async function getConfirmation(interaction, prompt, options) {
1142
1110
  return confirmed;
1143
1111
  }
1144
1112
 
1145
- //#endregion
1146
- //#region src/interfaces/Plugin.ts
1147
- /**
1148
- * Base class for Seedcord plugins
1149
- *
1150
- * Extend this class to create plugins that integrate with the Seedcord lifecycle.
1151
- * Plugins have access to the core instance and must implement initialization logic.
1152
- */
1153
- var Plugin = class extends _seedcord_event_emitter.TypedEventEmitter {
1154
- pluggable;
1155
- name = this.constructor.name;
1156
- constructor(pluggable) {
1157
- super();
1158
- this.pluggable = pluggable;
1159
- }
1160
- /**
1161
- * Reloads plugin state on an HMR update. The base implementation is a no-op, override it in your plugin.
1162
- * @virtual
1163
- */
1164
- onHmr(_event) {
1165
- return Promise.resolve();
1166
- }
1167
- /**
1168
- * Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
1169
- * @param patterns - Glob patterns relative to the project root
1170
- */
1171
- registerCriticalFiles(patterns) {
1172
- (0, _seedcord_core_internal.getDevChannel)()?.send("seedcord:register-critical-files", { patterns });
1173
- }
1174
- /** @internal */
1175
- removeListener(event, listener) {
1176
- return super.removeListener(event, listener);
1177
- }
1178
- /** @internal */
1179
- removeAllListeners(event) {
1180
- return super.removeAllListeners(event);
1181
- }
1182
- };
1183
- /**
1184
- * Base class for objects that can have plugins attached.
1185
- *
1186
- * Provides plugin attachment capabilities and lifecycle management. Plugins are attached during
1187
- * configuration and initialized during startup. Not constructed directly, the host is a {@link Seedcord}.
1188
- */
1189
- var Pluggable = class Pluggable extends _seedcord_event_emitter.TypedEventEmitter {
1190
- isInitialized = false;
1191
- shutdown;
1192
- startup;
1193
- plugins = [];
1194
- static PLUGIN_INIT_TIMEOUT_MS = 15e3;
1195
- constructor(shutdown, startup) {
1196
- super();
1197
- this.shutdown = shutdown;
1198
- this.startup = startup;
1199
- }
1200
- /** @internal */
1201
- async init() {
1202
- if (this.isInitialized) return this;
1203
- await this.startup.run();
1204
- this.isInitialized = true;
1205
- return this;
1206
- }
1207
- /**
1208
- * Attaches a plugin to this instance
1209
- *
1210
- * Plugins provide external functionality and are initialized during the specified startup phase.
1211
- * The plugin instance becomes available as a property in `core` wherever it's available.
1212
- *
1213
- * Make sure to augment the {@link Core} interface with the plugin type to ensure TypeScript recognizes it and provides intellisense.
1214
- *
1215
- * @typeParam Key - The property name for accessing the plugin
1216
- * @typeParam Ctor - The plugin constructor type
1217
- * @param key - Property name to access the plugin instance
1218
- * @param Plugin - Plugin constructor class
1219
- * @param startupPhase - When during startup to initialize this plugin ({@link StartupPhase})
1220
- * @param args - Additional arguments to pass to the plugin constructor
1221
- * @returns This instance with the plugin attached as a typed property
1222
- * @throws A **SeedcordError** When called after initialization or if key already exists
1223
- * @example
1224
- * ```typescript
1225
- * seedcord.attach('db', Mongo, StartupPhase.Configuration, { uri: 'mongodb://...', name: 'seedcord', dir: ... })
1226
- * ```
1227
- */
1228
- attach(key, Plugin, startupPhase, ...args) {
1229
- if (this.isInitialized) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CorePluginAfterInit);
1230
- if (key in this) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CorePluginKeyExists, [key]);
1231
- const instance = new Plugin(this, ...args);
1232
- this.plugins.push(instance);
1233
- const entry = { [key]: instance };
1234
- this.startup.addTask(startupPhase, `Plugin:${key}`, async () => {
1235
- instance.logger.utils.initialization(key, "start");
1236
- await instance.init();
1237
- instance.logger.utils.initialization(key, "end");
1238
- }, Pluggable.PLUGIN_INIT_TIMEOUT_MS);
1239
- return Object.assign(this, entry);
1240
- }
1241
- /** @internal */
1242
- removeListener(event, listener) {
1243
- return super.removeListener(event, listener);
1244
- }
1245
- /** @internal */
1246
- removeAllListeners(event) {
1247
- return super.removeAllListeners(event);
1248
- }
1249
- };
1250
-
1251
1113
  //#endregion
1252
1114
  //#region src/handlers/BaseHandler.ts
1253
1115
  /**
@@ -1273,7 +1135,7 @@ var BaseHandler = class extends _seedcord_core.BaseHandler {
1273
1135
  */
1274
1136
  var InteractionMiddleware = class extends BaseHandler {
1275
1137
  constructor(event, core) {
1276
- super(event, core);
1138
+ super(event, core, void 0, "interactions");
1277
1139
  }
1278
1140
  };
1279
1141
 
@@ -1293,8 +1155,8 @@ var RepliableHandler = class extends _seedcord_core.RepliableHandler {
1293
1155
  getEvent() {
1294
1156
  return this.event;
1295
1157
  }
1296
- buildSender(event, _core, routeId) {
1297
- return new ReplySender(event, routeId);
1158
+ buildSender(event, core, routeId) {
1159
+ return new ReplySender(event, routeId, core.bus);
1298
1160
  }
1299
1161
  };
1300
1162
 
@@ -1449,7 +1311,7 @@ var ContextMenuHandler = class extends InteractionHandler {
1449
1311
  */
1450
1312
  var AutocompleteHandler = class extends BaseHandler {
1451
1313
  constructor(event, core, dispatch) {
1452
- super(event, core, dispatch);
1314
+ super(event, core, dispatch, "interactions");
1453
1315
  }
1454
1316
  decodedFocused;
1455
1317
  /**
@@ -1500,7 +1362,11 @@ var AutocompleteHandler = class extends BaseHandler {
1500
1362
  }
1501
1363
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1502
1364
  async respond(choices) {
1503
- await this.event.respond(choices);
1365
+ const routeId = this.dispatch?.routeId ?? "autocomplete";
1366
+ await (0, _seedcord_core_internal.reportedWrite)({
1367
+ bus: this.core.bus,
1368
+ interactionId: this.event.id
1369
+ }, routeId, "respond", () => this.event.respond(choices));
1504
1370
  }
1505
1371
  /** The firing command route, for a field whose completion differs per registered command. */
1506
1372
  get route() {
@@ -1693,7 +1559,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
1693
1559
  var EventHandler = class extends BaseHandler {
1694
1560
  firedEvent;
1695
1561
  constructor(event, core, eventName) {
1696
- super(event, core);
1562
+ super(event, core, void 0, "events");
1697
1563
  this.firedEvent = eventName;
1698
1564
  }
1699
1565
  /**
@@ -1746,7 +1612,7 @@ var EventHandler = class extends BaseHandler {
1746
1612
  var EventMiddleware = class extends BaseHandler {
1747
1613
  firedEvent;
1748
1614
  constructor(event, core, eventName) {
1749
- super(event, core);
1615
+ super(event, core, void 0, "events");
1750
1616
  this.firedEvent = eventName;
1751
1617
  }
1752
1618
  /**
@@ -1931,10 +1797,15 @@ var Paginator = class {
1931
1797
  }
1932
1798
  };
1933
1799
  }
1934
- /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
1935
- async start(interaction, core) {
1936
- const response = await this.page(contextOf(interaction, core), 0);
1937
- return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
1800
+ /**
1801
+ * Render page 0 and send it through the handler's sender, picking reply or followUp from its ack state.
1802
+ *
1803
+ * @param handler - The handler starting the paginator, normally `this`.
1804
+ */
1805
+ async start(handler) {
1806
+ const interaction = handler.getEvent();
1807
+ const response = await this.page(contextOf(interaction, handler.core), 0);
1808
+ return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1938
1809
  }
1939
1810
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
1940
1811
  async page(ctx, n) {
@@ -1994,250 +1865,32 @@ var CursorSource = class {
1994
1865
  };
1995
1866
 
1996
1867
  //#endregion
1997
- //#region src/subscribers/Subscriber.ts
1868
+ //#region src/subscribers/index.ts
1998
1869
  /**
1999
- * Abstract base class for handling application subscribers.
2000
- *
2001
- * Provides type-safe access to subscribers data and the core framework instance.
2002
- * Extend this class to create custom subscriber handlers.
1870
+ * Base class for a gateway subscriber, binding `this.core` to the gateway {@link Core}.
2003
1871
  *
2004
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
1872
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
2005
1873
  */
2006
- var Subscriber = class {
2007
- data;
2008
- core;
2009
- logger;
2010
- /**
2011
- * Instantiates the subscriber with event data and core.
2012
- *
2013
- * @param data - The subscriber event data
2014
- * @param core - The core framework instance
2015
- */
2016
- constructor(data, core) {
2017
- this.data = data;
2018
- this.core = core;
2019
- this.logger = new _seedcord_logger.Logger(this.constructor.name);
2020
- }
2021
- };
2022
-
2023
- //#endregion
2024
- //#region src/subscribers/decorators/Subscribe.ts
1874
+ var Subscriber = class extends _seedcord_core.Subscriber {};
2025
1875
  /**
2026
- * Registers a subscriber handler class with a specific subscriber event.
1876
+ * Base class for a gateway subscriber that delivers its event to a Discord webhook, binding
1877
+ * `this.core` to the gateway {@link Core}.
2027
1878
  *
2028
- * Associates the decorated class with a subscriber event type for automatic
2029
- * registration and execution when the subscriber is emitted.
2030
- *
2031
- * @typeParam TSubscriber - The subscriber event name to register for
2032
- * @param subscriber - The subscriber event name to register for
2033
- * @param options - Options to configure the subscriber handler registration.
2034
- * @decorator
2035
- * @example
2036
- * ```typescript
2037
- * \@Subscribe('userJoin')
2038
- * class WelcomeHandler extends Subscriber<'userJoin'> {
2039
- * async execute() {
2040
- * // Handle user join event
2041
- * }
2042
- * }
2043
- * ```
2044
- * @example
2045
- * ```ts
2046
- * \@Subscribe('userJoin', { frequency: 'once' })
2047
- * // or
2048
- * \@Subscribe('userJoin')
2049
- * ```
1879
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2050
1880
  */
2051
- function Subscribe(subscriber, options) {
2052
- return function(constructor) {
2053
- const meta = {
2054
- subscriber,
2055
- frequency: options?.frequency
2056
- };
2057
- Reflect.defineMetadata(_seedcord_core_internal.SubscribeMetadataKey, meta, constructor);
2058
- };
2059
- }
1881
+ var WebhookLog = class extends _seedcord_core.WebhookLog {};
2060
1882
 
2061
1883
  //#endregion
2062
- //#region src/subscribers/decorators/WebhookUrl.ts
1884
+ //#region src/plugin.ts
2063
1885
  /**
2064
- * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
1886
+ * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
2065
1887
  *
2066
- * An unset variable disables the reporter with a boot warning. A set but malformed value throws
2067
- * at boot.
1888
+ * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
1889
+ * `this.core` carries the shared members only.
2068
1890
  *
2069
- * @param envKey - The environment variable name holding the webhook url
2070
- * @decorator
2071
- * @example
2072
- * ```ts
2073
- * \@Subscribe('memberBanned')
2074
- * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
2075
- * class BanLog extends WebhookLog<'memberBanned'> {
2076
- * report() {
2077
- * return { components: [new BanCard(this.data).component] };
2078
- * }
2079
- * }
2080
- * ```
1891
+ * @typeParam Opts - The transport, runtime, and capabilities the plugin declares.
2081
1892
  */
2082
- function WebhookUrl(envKey) {
2083
- return function(constructor) {
2084
- Reflect.defineMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, envKey, constructor);
2085
- };
2086
- }
2087
-
2088
- //#endregion
2089
- //#region src/miscellaneous/flagsFor.ts
2090
- /** @internal */
2091
- function flagsFor(ephemeral) {
2092
- let flags = discord_api_types_v10.MessageFlags.IsComponentsV2;
2093
- if (ephemeral) flags |= discord_api_types_v10.MessageFlags.Ephemeral;
2094
- return flags;
2095
- }
2096
-
2097
- //#endregion
2098
- //#region src/subscribers/bases/webhookHelpers.ts
2099
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
2100
- function isDiscordWebhookUrl(value) {
2101
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
2102
- }
2103
- function jsonAttachment(name, description, data) {
2104
- const content = (0, _seedcord_utils.filterCirculars)(data);
2105
- return {
2106
- name,
2107
- description,
2108
- data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
2109
- };
2110
- }
2111
- function neutralizeFences(text) {
2112
- return text.replaceAll("```", "`​`​`");
2113
- }
2114
- /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
2115
- function errorReport(error) {
2116
- let report = error.stack ?? `${error.name}: ${error.message}`;
2117
- if (Error.isError(error.cause)) {
2118
- const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
2119
- report += `\n\nCaused by:\n${cause}`;
2120
- }
2121
- const stripped = neutralizeFences((0, _seedcord_utils.stripAnsi)(report));
2122
- return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
2123
- }
2124
- var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
2125
- constructor() {
2126
- super("separator");
2127
- this.instance.setSpacing(discord_api_types_v10.SeparatorSpacingSize.Small).setDivider(true);
2128
- }
2129
- };
2130
-
2131
- //#endregion
2132
- //#region src/subscribers/bases/WebhookSender.ts
2133
- const HTTP_NOT_FOUND = 404;
2134
- const HTTP_UNAUTHORIZED = 401;
2135
- /**
2136
- * Sends component messages to a Discord webhook through `@discordjs/rest`.
2137
- *
2138
- * @throws A `SeedcordError` when the url does not carry a webhook id and token.
2139
- */
2140
- var WebhookSender = class {
2141
- rest = new _discordjs_rest.REST();
2142
- route;
2143
- constructor(url) {
2144
- const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2145
- if (!match?.[1] || !match[2]) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2146
- this.route = discord_api_types_v10.Routes.webhook(match[1], match[2]);
2147
- }
2148
- /**
2149
- * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2150
- * Discord and `unreachable` on any other failure.
2151
- */
2152
- async verify() {
2153
- try {
2154
- await this.rest.get(this.route, { auth: false });
2155
- return "ok";
2156
- } catch (error) {
2157
- return error instanceof _discordjs_rest.DiscordAPIError && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2158
- }
2159
- }
2160
- async send(options) {
2161
- const { flags, username, components, files } = options;
2162
- await this.rest.post(this.route, {
2163
- auth: false,
2164
- query: new URLSearchParams({
2165
- with_components: "true",
2166
- wait: "true"
2167
- }),
2168
- body: {
2169
- flags,
2170
- username,
2171
- components: components.map((component) => component.toJSON()),
2172
- ...files?.length && { attachments: files.map((file, id) => ({
2173
- id,
2174
- filename: file.name,
2175
- description: file.description
2176
- })) }
2177
- },
2178
- ...files?.length && { files: files.map((file) => ({
2179
- name: file.name,
2180
- data: file.data
2181
- })) }
2182
- });
2183
- }
2184
- };
2185
-
2186
- //#endregion
2187
- //#region src/subscribers/bases/WebhookLog.ts
2188
- /**
2189
- * Base class for subscribers that deliver their event to a Discord webhook.
2190
- *
2191
- * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2192
- * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2193
- * the reporter is skipped at registration with a boot warning.
2194
- *
2195
- * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2196
- */
2197
- var WebhookLog = class WebhookLog extends Subscriber {
2198
- static senders = /* @__PURE__ */ new Map();
2199
- async execute() {
2200
- const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2201
- if (url === null) {
2202
- this.logger.warn(`${chalk.default.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2203
- return;
2204
- }
2205
- const { username, components, files } = await this.report();
2206
- try {
2207
- await WebhookLog.senderFor(url).send({
2208
- flags: flagsFor(false),
2209
- username: username ?? this.constructor.name,
2210
- components,
2211
- files
2212
- });
2213
- } catch (error) {
2214
- this.logger.error("Failed to send webhook report", error);
2215
- }
2216
- }
2217
- /** @internal */
2218
- static senderFor(url) {
2219
- let sender = WebhookLog.senders.get(url);
2220
- if (!sender) {
2221
- sender = new WebhookSender(url);
2222
- WebhookLog.senders.set(url, sender);
2223
- }
2224
- return sender;
2225
- }
2226
- /** @internal */
2227
- static envKeyOf(ctor) {
2228
- const envKey = Reflect.getMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, ctor);
2229
- if (!envKey) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2230
- return envKey;
2231
- }
2232
- /** @internal Returns null when the env var is unset, throws when set but malformed. */
2233
- static urlOf(envKey) {
2234
- if (!envapt.Envapter.has(envKey)) return null;
2235
- const raw = envapt.Envapter.getRequired(envKey, envapt.Converters.String).trim();
2236
- if (raw === "") return null;
2237
- if (!isDiscordWebhookUrl(raw)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2238
- return raw;
2239
- }
2240
- };
1893
+ var Plugin = class extends _seedcord_core_plugin.Plugin {};
2241
1894
 
2242
1895
  //#endregion
2243
1896
  //#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
@@ -2293,8 +1946,7 @@ function slashRouteLeaves(commands) {
2293
1946
  var CommandRegistry = class {
2294
1947
  core;
2295
1948
  onDeployed;
2296
- name = "Commands";
2297
- logger = new _seedcord_logger.Logger("Commands");
1949
+ logger = new _seedcord_logger.Logger("Commands", { channel: "commands" });
2298
1950
  isInitialised = false;
2299
1951
  globalCommands = [];
2300
1952
  guildCommands = new discord_js.Collection();
@@ -2381,7 +2033,7 @@ var CommandRegistry = class {
2381
2033
  this.registerCommand(exported, rel);
2382
2034
  this.hmrHandler?.trackHandler(fullPath, exported);
2383
2035
  }
2384
- }, this.logger);
2036
+ });
2385
2037
  }
2386
2038
  isCommandClass(obj) {
2387
2039
  if (typeof obj !== "function") return false;
@@ -2543,7 +2195,7 @@ function eventGateContext(eventName, args, core) {
2543
2195
 
2544
2196
  //#endregion
2545
2197
  //#region src/bot/handleEventFault.ts
2546
- const logger$1 = new _seedcord_logger.Logger("EventBoundary");
2198
+ const logger$1 = new _seedcord_logger.Logger("EventBoundary", { channel: "errors" });
2547
2199
  /**
2548
2200
  * The event controller boundary. Reports a throw from an event handler and never replies, since a
2549
2201
  * generic event has no reply target. A {@link Silence} or a non-reporting {@link Notice} stops quietly, a
@@ -2556,15 +2208,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2556
2208
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2557
2209
  return;
2558
2210
  }
2559
- if (!Error.isError(caught)) throw caught;
2211
+ const error = (0, _seedcord_core_internal.asError)(caught);
2560
2212
  const ignore = new Set(core.config.errors?.ignoreEventApiCodes ?? []);
2561
- if (caught instanceof discord_js.DiscordAPIError && ignore.has(caught.code)) {
2562
- logger$1.debug(`swallowed api code ${caught.code}`);
2213
+ if (error instanceof discord_js.DiscordAPIError && ignore.has(error.code)) {
2214
+ logger$1.debug(`swallowed api code ${error.code}`);
2563
2215
  return;
2564
2216
  }
2565
- if (caught instanceof _seedcord_core.Notice && !caught.report) return;
2217
+ if (error instanceof _seedcord_core.Notice && !error.report) return;
2566
2218
  const actor = deriveEventActor(args);
2567
- extractErrorResponse(caught, core, {
2219
+ extractErrorResponse(error, core, {
2568
2220
  event: {
2569
2221
  name: eventName,
2570
2222
  handler: handlerName,
@@ -2579,21 +2231,22 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2579
2231
  //#endregion
2580
2232
  //#region src/bot/controllers/EventDispatcher.ts
2581
2233
  /**
2582
- * Scans event handler directories, registers handlers with Discord client events,
2583
- * and coordinates event execution through the handler system. Does not handle interactions.
2584
- * Multiple handlers can point to one event.
2234
+ * Scans event handler directories, attaches one client listener per registered event, and runs the
2235
+ * handlers for each fire. Interactions route through InteractionDispatcher. Multiple handlers can
2236
+ * point to one event.
2585
2237
  *
2586
2238
  * @internal
2587
2239
  */
2588
2240
  var EventDispatcher = class {
2589
2241
  core;
2590
- logger = new _seedcord_logger.Logger("Events");
2242
+ logger = new _seedcord_logger.Logger("Events", { channel: "events" });
2591
2243
  isInitialized = false;
2592
- name = "Events";
2593
2244
  eventMap = /* @__PURE__ */ new Map();
2594
2245
  middlewares = [];
2595
2246
  executedOnceHandlers = /* @__PURE__ */ new Set();
2596
2247
  attachedEvents = /* @__PURE__ */ new Set();
2248
+ inFlight = /* @__PURE__ */ new Set();
2249
+ draining = false;
2597
2250
  loading = false;
2598
2251
  loadedHandlers = [];
2599
2252
  loadedMiddlewares = [];
@@ -2657,7 +2310,7 @@ var EventDispatcher = class {
2657
2310
  this.registerHandler(val, relativePath);
2658
2311
  this.hmrHandler?.trackHandler(fullPath, val);
2659
2312
  }
2660
- }, this.logger);
2313
+ });
2661
2314
  }
2662
2315
  async loadMiddlewares(dir) {
2663
2316
  await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
@@ -2666,7 +2319,7 @@ var EventDispatcher = class {
2666
2319
  this.registerMiddleware(val, relativePath);
2667
2320
  this.hmrHandler?.trackMiddleware(fullPath, val);
2668
2321
  }
2669
- }, this.logger);
2322
+ });
2670
2323
  }
2671
2324
  /** @internal For use in dev mode */
2672
2325
  async onHmr(event) {
@@ -2762,15 +2415,26 @@ var EventDispatcher = class {
2762
2415
  const handlerEntries = this.eventMap.get(eventName);
2763
2416
  this.logger.debug(`Attaching ${_seedcord_logger.paint.mint.bold(eventName)} to the client with ${_seedcord_logger.paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2764
2417
  this.core.bot.client.on(eventName, (...args) => {
2765
- this.core.bot.emitSafe("any:event", eventName, ...args);
2766
- (async () => {
2767
- await this.processEvent(eventName, args).catch((err) => {
2768
- this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2769
- this.core.bot.emit("error:unhandled:event", err);
2770
- });
2771
- })();
2418
+ if (this.draining) return;
2419
+ this.core.bus[_seedcord_core_internal.PublishDefault]("anyEvent", {
2420
+ name: eventName,
2421
+ args
2422
+ });
2423
+ const run = this.processEvent(eventName, args).catch((caught) => {
2424
+ const error = (0, _seedcord_core_internal.asError)(caught);
2425
+ this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2426
+ this.core.bus[_seedcord_core_internal.PublishDefault]("unhandledEventError", { error });
2427
+ });
2428
+ this.inFlight.add(run);
2429
+ run.finally(() => this.inFlight.delete(run));
2772
2430
  });
2773
2431
  }
2432
+ stopAccepting() {
2433
+ this.draining = true;
2434
+ }
2435
+ drain(timeoutMs) {
2436
+ return (0, _seedcord_core_node_internal.settleWithin)(Promise.allSettled(this.inFlight), timeoutMs);
2437
+ }
2774
2438
  async processEvent(eventName, args) {
2775
2439
  const handlerEntries = this.eventMap.get(eventName);
2776
2440
  if (!handlerEntries || handlerEntries.length === 0) return;
@@ -2835,7 +2499,7 @@ const HARMLESS_API_CODES = new Set([
2835
2499
 
2836
2500
  //#endregion
2837
2501
  //#region src/bot/handleInteractionFault.ts
2838
- const logger = new _seedcord_logger.Logger("InteractionBoundary");
2502
+ const logger = new _seedcord_logger.Logger("InteractionBoundary", { channel: "errors" });
2839
2503
  /**
2840
2504
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
2841
2505
  * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
@@ -2844,33 +2508,45 @@ const logger = new _seedcord_logger.Logger("InteractionBoundary");
2844
2508
  *
2845
2509
  * @internal
2846
2510
  */
2847
- async function handleInteractionFault(caught, interaction, core, sender) {
2511
+ async function handleInteractionFault(caught, interaction, core, routeId, sender) {
2848
2512
  if (caught instanceof _seedcord_core.Silence) {
2849
2513
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2850
2514
  return;
2851
2515
  }
2852
- if (!Error.isError(caught)) throw caught;
2516
+ const error = (0, _seedcord_core_internal.asError)(caught);
2853
2517
  const ignore = new Set(core.config.errors?.ignoreApiCodes ?? []);
2854
- if (caught instanceof discord_js.DiscordAPIError && ignore.has(caught.code)) {
2855
- logger.debug(`swallowed api code ${caught.code}`);
2518
+ if (error instanceof discord_js.DiscordAPIError && ignore.has(error.code)) {
2519
+ logger.debug(`swallowed api code ${error.code}`);
2856
2520
  return;
2857
2521
  }
2858
2522
  if (interaction.isAutocomplete()) {
2859
- extractErrorResponse(caught, core, {
2523
+ extractErrorResponse(error, core, {
2860
2524
  guild: interaction.guild,
2861
2525
  user: interaction.user,
2862
2526
  metadata: interaction,
2863
2527
  route: slashRouteOf(interaction)
2864
2528
  });
2529
+ await sendEmptyChoices(interaction, core, routeId);
2865
2530
  return;
2866
2531
  }
2867
- const { response } = extractErrorResponse(caught, core, {
2532
+ const { response } = extractErrorResponse(error, core, {
2868
2533
  interaction,
2869
2534
  guild: interaction.guild,
2870
2535
  user: interaction.user,
2871
2536
  metadata: interaction
2872
2537
  });
2873
- await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
2538
+ await sendGuarded(sender ?? new ReplySender(interaction, routeId, core.bus), response, error instanceof _seedcord_core.Notice ? error.ephemeral : true);
2539
+ }
2540
+ async function sendEmptyChoices(interaction, core, routeId) {
2541
+ const telemetry = {
2542
+ bus: core.bus,
2543
+ interactionId: interaction.id
2544
+ };
2545
+ try {
2546
+ await (0, _seedcord_core_internal.reportedWrite)(telemetry, routeId, "respond", () => interaction.respond([]));
2547
+ } catch (error) {
2548
+ logger.debug(`autocomplete empty-choices send failed: ${String(error)}`);
2549
+ }
2874
2550
  }
2875
2551
  async function sendGuarded(sender, response, ephemeral) {
2876
2552
  try {
@@ -2894,9 +2570,8 @@ async function sendGuarded(sender, response, ephemeral) {
2894
2570
  */
2895
2571
  var InteractionDispatcher = class {
2896
2572
  core;
2897
- logger = new _seedcord_logger.Logger("Interactions");
2573
+ logger = new _seedcord_logger.Logger("Interactions", { channel: "interactions" });
2898
2574
  isInitialized = false;
2899
- name = "Interactions";
2900
2575
  slashMap = /* @__PURE__ */ new Map();
2901
2576
  buttonMap = /* @__PURE__ */ new Map();
2902
2577
  modalMap = /* @__PURE__ */ new Map();
@@ -2908,8 +2583,11 @@ var InteractionDispatcher = class {
2908
2583
  messageContextMenuMap = /* @__PURE__ */ new Map();
2909
2584
  userContextMenuMap = /* @__PURE__ */ new Map();
2910
2585
  autocompleteMap = /* @__PURE__ */ new Map();
2586
+ handlerFiles = /* @__PURE__ */ new Map();
2911
2587
  keysToIgnore = /* @__PURE__ */ new Set();
2912
2588
  middlewares = [];
2589
+ inFlight = /* @__PURE__ */ new Set();
2590
+ draining = false;
2913
2591
  loading = false;
2914
2592
  loadedHandlers = [];
2915
2593
  loadedMiddlewares = [];
@@ -3024,7 +2702,7 @@ var InteractionDispatcher = class {
3024
2702
  this.registerHandler(val, relativePath);
3025
2703
  this.hmrHandler?.trackHandler(fullPath, val);
3026
2704
  }
3027
- }, this.logger);
2705
+ });
3028
2706
  }
3029
2707
  async loadMiddlewares(dir) {
3030
2708
  await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
@@ -3033,7 +2711,7 @@ var InteractionDispatcher = class {
3033
2711
  this.registerMiddleware(val, relativePath);
3034
2712
  this.hmrHandler?.trackMiddleware(fullPath, val);
3035
2713
  }
3036
- }, this.logger);
2714
+ });
3037
2715
  }
3038
2716
  registerMiddleware(middlewareCtor, relativePath) {
3039
2717
  const metadata = Reflect.getMetadata(_seedcord_core_internal.MiddlewareMetadataKey, middlewareCtor);
@@ -3063,6 +2741,7 @@ var InteractionDispatcher = class {
3063
2741
  return obj.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(_seedcord_core_internal.MiddlewareMetadataKey, obj);
3064
2742
  }
3065
2743
  registerHandler(handlerClass, relativePath) {
2744
+ const from = (0, _seedcord_utils.formatFilePath)(relativePath);
3066
2745
  const writes = [];
3067
2746
  for (const [routeType, map] of this.routeTypes) {
3068
2747
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
@@ -3070,16 +2749,16 @@ var InteractionDispatcher = class {
3070
2749
  for (const route of meta) {
3071
2750
  const existing = map.get(route);
3072
2751
  if (existing && existing !== handlerClass) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
3073
- route,
3074
- existing.name,
3075
- handlerClass.name
2752
+ `${routeType}:${route}`,
2753
+ `${existing.name} (${this.handlerFiles.get(existing) ?? "unknown file"})`,
2754
+ `${handlerClass.name} (${from})`
3076
2755
  ]);
3077
2756
  writes.push([map, route]);
3078
2757
  }
3079
2758
  }
3080
2759
  if (writes.length === 0) return;
3081
2760
  for (const [map, route] of writes) map.set(route, handlerClass);
3082
- const from = (0, _seedcord_utils.formatFilePath)(relativePath);
2761
+ this.handlerFiles.set(handlerClass, from);
3083
2762
  if (this.loading) this.loadedHandlers.push({
3084
2763
  name: handlerClass.name,
3085
2764
  from
@@ -3091,6 +2770,7 @@ var InteractionDispatcher = class {
3091
2770
  await this.hmrHandler?.handle(event);
3092
2771
  }
3093
2772
  unregisterHandler(handlerClass, artifacts) {
2773
+ this.handlerFiles.delete(handlerClass);
3094
2774
  if (artifacts) {
3095
2775
  for (const { routeType, routes } of artifacts) {
3096
2776
  const map = this.routeTypes.find(([type]) => type === routeType)?.[1];
@@ -3110,44 +2790,92 @@ var InteractionDispatcher = class {
3110
2790
  }
3111
2791
  attachToClient() {
3112
2792
  this.core.bot.client.on(discord_js.Events.InteractionCreate, (interaction) => {
3113
- this.core.bot.emitSafe("any:interaction", interaction);
3114
- this.handleInteraction(interaction).catch((err) => {
3115
- this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3116
- this.core.bot.emit("error:unhandled:interaction", err);
2793
+ if (this.draining) return;
2794
+ this.core.bus[_seedcord_core_internal.PublishDefault]("anyInteraction", { interaction });
2795
+ const run = this.handleInteraction(interaction).catch((caught) => {
2796
+ const error = (0, _seedcord_core_internal.asError)(caught);
2797
+ this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2798
+ this.core.bus[_seedcord_core_internal.PublishDefault]("unhandledInteractionError", { error });
3117
2799
  });
2800
+ this.inFlight.add(run);
2801
+ run.finally(() => this.inFlight.delete(run));
3118
2802
  });
3119
2803
  }
3120
- async handleCustomIdInteraction(interaction, getMap, interactionType) {
2804
+ stopAccepting() {
2805
+ this.draining = true;
2806
+ }
2807
+ drain(timeoutMs) {
2808
+ return (0, _seedcord_core_node_internal.settleWithin)(Promise.allSettled(this.inFlight), timeoutMs);
2809
+ }
2810
+ async handleCustomIdInteraction(interaction, kind, getMap) {
3121
2811
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
3122
2812
  const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.customId);
3123
- if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3124
- await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
2813
+ if (!prefix) this.logger.warn(`${_seedcord_logger.paint.sky.bold(kind)} has invalid customId: ${_seedcord_logger.paint.mute(interaction.customId)}`);
2814
+ await this.processInteraction(interaction, kind, () => prefix, (key) => getMap().get(key));
3125
2815
  }
3126
- async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
2816
+ async processInteraction(interaction, kind, extractKey, getHandler, fallback = UnhandledRepliable) {
3127
2817
  const key = extractKey(interaction);
2818
+ const startedAt = performance.now();
2819
+ const queuedMs = (0, _seedcord_core_internal.queuedMsFor)(interaction.id);
2820
+ const matched = getHandler(key);
2821
+ let routeId = `${kind}:${key || "unrouted"}`;
2822
+ let reported = false;
2823
+ const report = (outcome) => {
2824
+ if (reported) return;
2825
+ reported = true;
2826
+ this.core.bus[_seedcord_core_internal.PublishDefault]("interactionDispatched", (0, _seedcord_core_internal.dispatchedPayload)({
2827
+ routeId,
2828
+ interactionId: interaction.id,
2829
+ kind,
2830
+ outcome,
2831
+ startedAt,
2832
+ fallback: !matched,
2833
+ queuedMs
2834
+ }));
2835
+ };
3128
2836
  let sender;
3129
2837
  try {
3130
- if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3131
- let HandlerCtor = getHandler(key);
3132
- if (!HandlerCtor) {
3133
- this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3134
- HandlerCtor = fallback;
3135
- }
3136
- this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(key)} with ${_seedcord_logger.paint.mute(HandlerCtor.name)}`);
3137
- const dispatch = new _seedcord_core.DispatchContext((0, _seedcord_core_internal.routeIdOf)(HandlerCtor));
3138
- const handler = new HandlerCtor(interaction, this.core, dispatch);
2838
+ if (!interaction.isAutocomplete()) await this.runMiddlewares(interaction);
2839
+ const HandlerCtor = matched ?? fallback;
2840
+ const dispatch = new _seedcord_core.DispatchContext((0, _seedcord_core_internal.routeIdOf)(HandlerCtor) ?? routeId);
2841
+ routeId = dispatch.routeId ?? routeId;
2842
+ const handler = this.buildHandler(HandlerCtor, interaction, dispatch, key, !matched);
3139
2843
  if (handler instanceof RepliableHandler) sender = handler.getSender();
3140
- if (!interaction.isAutocomplete()) {
3141
- const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
3142
- try {
3143
- await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3144
- } finally {
3145
- monitor?.report(dispatch.routeId);
3146
- }
2844
+ const refusal = interaction.isAutocomplete() ? null : await this.gateRefusal(HandlerCtor, interaction, dispatch.routeId);
2845
+ if (refusal) {
2846
+ await this.answer(refusal.caught, interaction, routeId, sender, report);
2847
+ return;
3147
2848
  }
3148
2849
  await handler.execute();
2850
+ report("handled");
2851
+ } catch (caught) {
2852
+ await this.answer(caught, interaction, routeId, sender, report);
2853
+ }
2854
+ }
2855
+ async answer(caught, interaction, routeId, sender, report) {
2856
+ try {
2857
+ await handleInteractionFault(caught, interaction, this.core, routeId, sender);
2858
+ } finally {
2859
+ report((0, _seedcord_core_internal.outcomeFor)(caught));
2860
+ }
2861
+ }
2862
+ buildHandler(HandlerCtor, interaction, dispatch, key, isFallback) {
2863
+ if (isFallback) this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to ${HandlerCtor.name}.`);
2864
+ this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(key)} with ${_seedcord_logger.paint.mute(HandlerCtor.name)}`);
2865
+ return new HandlerCtor(interaction, this.core, dispatch);
2866
+ }
2867
+ async runMiddlewares(interaction) {
2868
+ for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
2869
+ }
2870
+ async gateRefusal(HandlerCtor, interaction, routeId) {
2871
+ const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
2872
+ try {
2873
+ await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core), routeId ?? void 0, monitor?.observe);
2874
+ return null;
3149
2875
  } catch (caught) {
3150
- await handleInteractionFault(caught, interaction, this.core, sender);
2876
+ return { caught };
2877
+ } finally {
2878
+ monitor?.report(routeId);
3151
2879
  }
3152
2880
  }
3153
2881
  async handleInteraction(interaction) {
@@ -3192,47 +2920,41 @@ var InteractionDispatcher = class {
3192
2920
  }
3193
2921
  async handleSlashCommand(interaction) {
3194
2922
  const route = slashRouteOf(interaction);
3195
- await this.processInteraction(interaction, () => route, (key) => this.slashMap.get(key));
2923
+ await this.processInteraction(interaction, _seedcord_core_internal.InteractionRoutes.Slash, () => route, (key) => this.slashMap.get(key));
3196
2924
  }
3197
2925
  async handleButton(interaction) {
3198
- await this.handleCustomIdInteraction(interaction, () => this.buttonMap, "Button");
2926
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.Button, () => this.buttonMap);
3199
2927
  }
3200
2928
  async handleModal(interaction) {
3201
- await this.handleCustomIdInteraction(interaction, () => this.modalMap, "Modal");
2929
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.Modal, () => this.modalMap);
3202
2930
  }
3203
2931
  async handleStringSelectMenu(interaction) {
3204
- await this.handleCustomIdInteraction(interaction, () => this.stringSelectMap, "String select menu");
2932
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.StringMenu, () => this.stringSelectMap);
3205
2933
  }
3206
2934
  async handleUserSelectMenu(interaction) {
3207
- await this.handleCustomIdInteraction(interaction, () => this.userSelectMap, "User select menu");
2935
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.UserMenu, () => this.userSelectMap);
3208
2936
  }
3209
2937
  async handleRoleSelectMenu(interaction) {
3210
- await this.handleCustomIdInteraction(interaction, () => this.roleSelectMap, "Role select menu");
2938
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.RoleMenu, () => this.roleSelectMap);
3211
2939
  }
3212
2940
  async handleChannelSelectMenu(interaction) {
3213
- await this.handleCustomIdInteraction(interaction, () => this.channelSelectMap, "Channel select menu");
2941
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.ChannelMenu, () => this.channelSelectMap);
3214
2942
  }
3215
2943
  async handleMentionableSelectMenu(interaction) {
3216
- await this.handleCustomIdInteraction(interaction, () => this.mentionableSelectMap, "Mentionable select menu");
2944
+ await this.handleCustomIdInteraction(interaction, _seedcord_core_internal.InteractionRoutes.MentionableMenu, () => this.mentionableSelectMap);
3217
2945
  }
3218
2946
  async handleMessageContextMenu(interaction) {
3219
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
2947
+ await this.processInteraction(interaction, _seedcord_core_internal.InteractionRoutes.MessageContextMenu, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
3220
2948
  }
3221
2949
  async handleUserContextMenu(interaction) {
3222
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
2950
+ await this.processInteraction(interaction, _seedcord_core_internal.InteractionRoutes.UserContextMenu, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
3223
2951
  }
3224
2952
  async handleAutocomplete(interaction) {
3225
2953
  const route = slashRouteOf(interaction);
3226
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
2954
+ await this.processInteraction(interaction, _seedcord_core_internal.InteractionRoutes.Autocomplete, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3227
2955
  }
3228
2956
  };
3229
2957
 
3230
- //#endregion
3231
- //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
3232
- function __decorateMetadata(k, v) {
3233
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3234
- }
3235
-
3236
2958
  //#endregion
3237
2959
  //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorate.js
3238
2960
  function __decorate(decorators, target, key, desc) {
@@ -3244,23 +2966,25 @@ function __decorate(decorators, target, key, desc) {
3244
2966
 
3245
2967
  //#endregion
3246
2968
  //#region src/bot/Bot.ts
2969
+ const DISPATCH_DRAIN_TIMEOUT_MS = 5e3;
2970
+ const DRAIN_TASK_TIMEOUT_MS = 6e3;
2971
+ const UNBIND_TIMEOUT_MS = 2e3;
2972
+ const LOGOUT_TIMEOUT_MS = 2e3;
3247
2973
  /**
3248
- * Discord bot implementation that manages client and controllers
3249
- *
3250
- * Don't instantiate this class directly. Use `core.bot` instead.
2974
+ * The Discord client and its controllers. Access it through `core.bot`.
3251
2975
  */
3252
- var Bot = class extends Plugin {
2976
+ var Bot = class {
3253
2977
  /** @internal */
3254
- logger = new _seedcord_logger.Logger("Bot");
2978
+ logger = new _seedcord_logger.Logger("Bot", { channel: "bot" });
3255
2979
  isInitialized = false;
3256
2980
  _client;
3257
2981
  interactions;
3258
2982
  events;
3259
- commands;
3260
2983
  emojiInjector;
2984
+ commands;
3261
2985
  emojis = Emojis;
3262
2986
  mentions = CommandMentions;
3263
- /** @internal For use in dev mode */
2987
+ /** @internal */
3264
2988
  async onHmr(event) {
3265
2989
  if (this.interactions) await this.interactions.onHmr(event);
3266
2990
  if (this.events) await this.events.onHmr(event);
@@ -3268,7 +2992,6 @@ var Bot = class extends Plugin {
3268
2992
  }
3269
2993
  /** @internal */
3270
2994
  constructor(core) {
3271
- super(core);
3272
2995
  this._client = new discord_js.Client(core.config.bot.clientOptions);
3273
2996
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
3274
2997
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
@@ -3279,12 +3002,24 @@ var Bot = class extends Plugin {
3279
3002
  });
3280
3003
  }
3281
3004
  this.emojiInjector = new EmojiInjector(core);
3282
- core.shutdown.addTask(_seedcord_core_node.ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3005
+ this.registerShutdownTasks(core);
3283
3006
  }
3284
- /**
3285
- * Initializes Discord client and all controllers
3286
- * @internal
3287
- */
3007
+ registerShutdownTasks(core) {
3008
+ core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Unbind, "stop-dispatch", () => {
3009
+ this.stopAccepting();
3010
+ return Promise.resolve();
3011
+ }, UNBIND_TIMEOUT_MS);
3012
+ core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Drain, "drain-dispatch", () => this.drain(), DRAIN_TASK_TIMEOUT_MS);
3013
+ core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Logout, "stop-bot", async () => await this.stop(), LOGOUT_TIMEOUT_MS);
3014
+ }
3015
+ stopAccepting() {
3016
+ this.interactions?.stopAccepting();
3017
+ this.events?.stopAccepting();
3018
+ }
3019
+ async drain() {
3020
+ await Promise.allSettled([this.interactions?.drain(DISPATCH_DRAIN_TIMEOUT_MS), this.events?.drain(DISPATCH_DRAIN_TIMEOUT_MS)]);
3021
+ }
3022
+ /** @internal */
3288
3023
  async init() {
3289
3024
  if (this.isInitialized) return;
3290
3025
  this.isInitialized = true;
@@ -3300,27 +3035,19 @@ var Bot = class extends Plugin {
3300
3035
  this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
3301
3036
  }
3302
3037
  }
3303
- /**
3304
- * Stops the bot and cleans up connections
3305
- * @internal
3306
- */
3038
+ /** @internal */
3307
3039
  async stop() {
3308
3040
  this._client.removeAllListeners();
3309
3041
  await this.logout();
3310
3042
  }
3311
- /**
3312
- * Logs the bot into Discord using the configured token
3313
- */
3314
3043
  async login(token) {
3315
- this._client.once(discord_js.Events.ClientReady, () => this.emit("ready"));
3044
+ const ready = Promise.withResolvers();
3045
+ this._client.once(discord_js.Events.ClientReady, () => ready.resolve());
3316
3046
  this._client.login(token);
3317
- await this.waitFor("ready");
3047
+ await ready.promise;
3318
3048
  this.logger.info(`Logged in as ${chalk.default.bold.magenta(this._client.user?.username)}!`);
3319
3049
  return this;
3320
3050
  }
3321
- /**
3322
- * Logs out and destroys the Discord client connection
3323
- */
3324
3051
  async logout() {
3325
3052
  await this._client.destroy();
3326
3053
  this.logger.info(chalk.default.bold.red("Logged out of Discord!"));
@@ -3328,250 +3055,21 @@ var Bot = class extends Plugin {
3328
3055
  get client() {
3329
3056
  return this._client;
3330
3057
  }
3331
- emit(event, ...args) {
3332
- return super.emit(event, ...args);
3333
- }
3334
- emitSafe(event, ...args) {
3335
- return super.emitSafe(event, ...args);
3336
- }
3337
- onListenerError(error, event) {
3338
- this.logger.error(`listener for ${String(event)} threw`, error);
3339
- }
3340
- };
3341
- __decorate([(0, envapt_legacy.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3342
-
3343
- //#endregion
3344
- //#region src/subscribers/default/HandledException.ts
3345
- let HandledException = class HandledException extends WebhookLog {
3346
- report() {
3347
- return {
3348
- username: "Handled Exception",
3349
- components: [new HandledExceptionContainer(this.data).component],
3350
- files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3351
- };
3352
- }
3353
- };
3354
- HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3355
- var HandledExceptionContainer = class extends _seedcord_core.BuilderComponent {
3356
- constructor(data) {
3357
- super("container");
3358
- const { denial, uuid, source } = data;
3359
- this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${neutralizeFences(causeStack(denial))}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
3360
- }
3361
- };
3362
- function faultSummary(denial, source) {
3363
- const head = (0, _seedcord_utils.stripAnsi)(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3364
- if (source.kind === "event") return `${head}**Event:** ${source.eventName}\n**Handler:** ${source.handler}\n**User ID:** \`${source.userId ?? "n/a"}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\``;
3365
- return `${head}**Kind:** ${source.interactionKind}\n**Command:** ${source.command ?? "n/a"}\n**CustomId:** ${source.customId ?? "n/a"}\n**User ID:** \`${source.userId}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\`\n**Interaction ID:** \`${source.interactionId}\``;
3366
- }
3367
- function causeStack(denial) {
3368
- const { cause } = denial;
3369
- if (Error.isError(cause)) return (0, _seedcord_utils.stripAnsi)(cause.stack ?? cause.message);
3370
- if (typeof cause === "string") return (0, _seedcord_utils.stripAnsi)(cause);
3371
- if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3372
- if (cause !== void 0) try {
3373
- return JSON.stringify(cause);
3374
- } catch {
3375
- return "[unserializable cause]";
3376
- }
3377
- return (0, _seedcord_utils.stripAnsi)(denial.stack ?? "No cause recorded.");
3378
- }
3379
-
3380
- //#endregion
3381
- //#region src/subscribers/default/UnknownException.ts
3382
- let UnknownException = class UnknownException extends WebhookLog {
3383
- report() {
3384
- const { metadata } = this.data;
3385
- return {
3386
- username: "Unknown Exception",
3387
- components: [new UnhandledErrorContainer(this.data).component],
3388
- files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3389
- };
3390
- }
3391
- };
3392
- UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3393
- var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
3394
- constructor(data) {
3395
- super("container");
3396
- const { uuid, error, guild, user, metadata } = data;
3397
- this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${errorReport(error)}\`\`\``));
3398
- this.addTimestampsIfAvailable(error);
3399
- this.addMetadataIfAvailable(metadata);
3400
- }
3401
- addTimestampsIfAvailable(error) {
3402
- if (!(error instanceof _discordjs_rest.DiscordAPIError)) return;
3403
- const now = Date.now();
3404
- const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3405
- if (!snowflake) return void 0;
3406
- const interactionTs = (0, _seedcord_utils.timestampFromSnowflake)(snowflake);
3407
- const diff = now - interactionTs;
3408
- const seconds = Math.floor(diff / 1e3);
3409
- const millis = diff % 1e3;
3410
- this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### Timestamps\n- **\`Interaction sent\` :** ${new Date(interactionTs).toISOString()} (${interactionTs})\n- **\`Error logged \` :** ${new Date(now).toISOString()} (${now})\n- **\`Offset \` :** ${seconds}s ${millis}ms`));
3411
- }
3412
- addMetadataIfAvailable(metadata) {
3413
- if (!metadata) return;
3414
- this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Metadata")).addFileComponents((file) => file.setURL("attachment://metadata.json"));
3415
- }
3416
- };
3417
-
3418
- //#endregion
3419
- //#region src/subscribers/Bus.ts
3420
- /**
3421
- * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3422
- * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3423
- */
3424
- var Bus = class extends Plugin {
3425
- core;
3426
- logger = new _seedcord_logger.Logger("Subscribers");
3427
- /** @internal */
3428
- name = "Subscribers";
3429
- isInitialized = false;
3430
- subscribersMap = /* @__PURE__ */ new Map();
3431
- executedOnceHandlers = /* @__PURE__ */ new Set();
3432
- webhookProbes = /* @__PURE__ */ new Map();
3433
- hmrHandler;
3434
- constructor(core) {
3435
- super(core);
3436
- this.core = core;
3437
- if (this.core.config.subscribers.path) {
3438
- if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
3439
- this.hmrHandler = new _seedcord_core_hmr.HmrModuleHandler({
3440
- handlersDir: this.core.config.subscribers.path,
3441
- isHandler: this.isSubscriber.bind(this),
3442
- registerHandler: this.registerSubscriber.bind(this),
3443
- unregisterHandler: this.unregisterSubscriber.bind(this),
3444
- getArtifacts: this.getArtifacts.bind(this),
3445
- logger: this.logger
3446
- });
3447
- }
3448
- }
3449
- getArtifacts(ctor) {
3450
- return [Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, ctor).subscriber];
3451
- }
3452
- /** @internal */
3453
- async init() {
3454
- if (this.isInitialized) return;
3455
- this.isInitialized = true;
3456
- this.registerSubscriber(UnknownException);
3457
- this.registerSubscriber(HandledException);
3458
- const subscribersDir = this.core.config.subscribers.path;
3459
- if (subscribersDir) {
3460
- this.logger.info(chalk.default.bold(subscribersDir));
3461
- await this.loadSubscribers(subscribersDir);
3462
- const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3463
- this.logger.utils.list([`${chalk.default.bold.magenta(totalSubscribers)} subscribers`], chalk.default.bold.green("Loaded"));
3464
- }
3465
- if (!envapt.Envapter.isTest) await this.verifyWebhooks();
3466
- }
3467
- /** @internal Exposed for tests. */
3468
- async verifyWebhooks() {
3469
- const missing = [];
3470
- await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3471
- const result = await WebhookLog.senderFor(url).verify();
3472
- if (result === "missing") missing.push(...envKeys);
3473
- else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${_seedcord_logger.paint.sky.bold(envKeys.join(", "))}`);
3474
- }));
3475
- if (missing.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3476
- }
3477
- /** @internal */
3478
- async onHmr(event) {
3479
- if (this.hmrHandler) await this.hmrHandler.handle(event);
3480
- }
3481
- async loadSubscribers(dir) {
3482
- await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
3483
- for (const exportName of Object.keys(imported)) {
3484
- const val = imported[exportName];
3485
- if (this.isSubscriber(val)) {
3486
- this.registerSubscriber(val);
3487
- this.hmrHandler?.trackHandler(fullPath, val);
3488
- this.logger.utils.registration(val.name, relativePath);
3489
- }
3490
- }
3491
- }, this.logger);
3492
- }
3493
- registerSubscriber(handler) {
3494
- if (handler.prototype instanceof WebhookLog) {
3495
- const envKey = WebhookLog.envKeyOf(handler);
3496
- const url = WebhookLog.urlOf(envKey);
3497
- if (url === null) {
3498
- this.logger.warn(`${chalk.default.bold(handler.name)} disabled, ${_seedcord_logger.paint.sky.bold(envKey)} is not set`);
3499
- return;
3500
- }
3501
- const envKeys = this.webhookProbes.get(url) ?? [];
3502
- if (!envKeys.includes(envKey)) envKeys.push(envKey);
3503
- this.webhookProbes.set(url, envKeys);
3504
- }
3505
- const options = Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, handler);
3506
- let handlers = this.subscribersMap.get(options.subscriber);
3507
- if (!handlers) {
3508
- handlers = [];
3509
- this.subscribersMap.set(options.subscriber, handlers);
3510
- }
3511
- handlers.push({
3512
- ctor: handler,
3513
- frequency: options.frequency ?? "on"
3514
- });
3515
- }
3516
- unregisterSubscriber(handler, artifacts) {
3517
- const subscribers = artifacts ?? [...this.subscribersMap.keys()];
3518
- for (const subscriber of subscribers) {
3519
- const handlers = this.subscribersMap.get(subscriber);
3520
- if (!handlers) continue;
3521
- const index = handlers.findIndex((entry) => entry.ctor === handler);
3522
- if (index !== -1) handlers.splice(index, 1);
3523
- }
3524
- }
3525
- isSubscriber(obj) {
3526
- if (typeof obj !== "function") return false;
3527
- return obj.prototype instanceof Subscriber && Reflect.hasMetadata(_seedcord_core_internal.SubscribeMetadataKey, obj);
3528
- }
3529
- /**
3530
- * Publishes an event to its subscribers and native listeners.
3531
- *
3532
- * Fire-and-forget, so subscriber handlers run asynchronously and this returns before they
3533
- * complete, and callers must not assume side effects have landed. Errors thrown by a subscriber
3534
- * are caught and logged, never surfaced here. A `'once'` subscriber is marked as executed when
3535
- * it starts (even if it throws), so it never runs twice.
3536
- *
3537
- * @param event - The subscription key to publish
3538
- * @param data - Payload passed to each subscriber
3539
- * @returns Whether the underlying emitter had native listeners for the event
3540
- */
3541
- publish(event, data) {
3542
- this.processSubscriber(event, data);
3543
- return super.emit(event, ...[data]);
3544
- }
3545
- async processSubscriber(subscriberName, data) {
3546
- const handlers = this.subscribersMap.get(subscriberName);
3547
- if (!handlers) return;
3548
- for (const entry of handlers) {
3549
- if (entry.frequency === "once" && this.executedOnceHandlers.has(entry.ctor)) continue;
3550
- try {
3551
- if (entry.frequency === "once") this.executedOnceHandlers.add(entry.ctor);
3552
- const Ctor = entry.ctor;
3553
- await new Ctor(data, this.core).execute();
3554
- } catch (err) {
3555
- this.logger.error(`Error in subscriber ${String(subscriberName)} handler ${entry.ctor.name}:`, err);
3556
- }
3557
- }
3558
- }
3559
3058
  };
3059
+ __decorate([(0, envapt_legacy.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) })], Bot.prototype, "botToken", void 0);
3560
3060
 
3561
3061
  //#endregion
3562
3062
  //#region src/version.ts
3563
3063
  /** Package version */
3564
- const version = "0.1.0-next.3";
3064
+ const version = "0.1.0-next.5";
3565
3065
 
3566
3066
  //#endregion
3567
3067
  //#region src/Seedcord.ts
3568
3068
  /**
3569
- * Main Seedcord bot framework class
3570
- *
3571
- * Primary entry point for creating Discord bots with Seedcord.
3572
- * Manages component lifecycle and provides plugin support.
3069
+ * The gateway bot host. Opens a discord.js gateway session, discovers handlers, and runs
3070
+ * coordinated startup and shutdown. Attach plugins with `attach()`.
3573
3071
  */
3574
- var Seedcord = class Seedcord extends Pluggable {
3072
+ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3575
3073
  config;
3576
3074
  /** @internal */
3577
3075
  [_seedcord_types_internal.SeedcordBrand] = true;
@@ -3579,83 +3077,86 @@ var Seedcord = class Seedcord extends Pluggable {
3579
3077
  augmentTarget = "@seedcord/gateway";
3580
3078
  /** @internal */
3581
3079
  version = version;
3582
- static isInstantiated = false;
3583
3080
  healthCheck;
3584
3081
  hmrManager;
3585
- /** @see {@link CoordinatedShutdown} */
3586
- shutdown;
3587
- /** @see {@link CoordinatedStartup} */
3588
- startup;
3589
3082
  /** @see {@link Bus} */
3590
3083
  bus;
3084
+ subscribers;
3591
3085
  /** @see {@link Bot} */
3592
3086
  bot;
3593
3087
  /** @see {@link IRateLimiter} */
3594
3088
  rateLimiter;
3595
3089
  /**
3596
- * Creates a new Seedcord instance
3597
- *
3598
3090
  * @param config - Bot configuration including paths and Discord client options
3599
3091
  * @throws A **SeedcordError** When attempting to create multiple instances (singleton)
3600
3092
  */
3601
3093
  constructor(config) {
3602
- if (Seedcord.isInstantiated) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreSingletonViolation);
3603
- Seedcord.isInstantiated = true;
3604
- const shutdown = new _seedcord_core_node.CoordinatedShutdown(config.shutdownEnabled);
3605
- const startup = new _seedcord_core_node.CoordinatedStartup();
3606
- super(shutdown, startup);
3094
+ super(new _seedcord_core_node_internal.CoordinatedShutdown(), new _seedcord_core_node_internal.CoordinatedStartup());
3607
3095
  this.config = config;
3608
3096
  (0, _seedcord_logger_node.installNodeDefaults)(config.logger);
3609
3097
  (0, _seedcord_core_internal.setBotColor)(config.botColor);
3610
- this.shutdown = shutdown;
3611
- this.startup = startup;
3612
3098
  this.hmrManager = new _seedcord_core_internal.HmrManager();
3613
3099
  this.hmrManager.init();
3614
- this.bus = new Bus(this);
3100
+ this.bus = new _seedcord_core.Bus(this);
3101
+ this.subscribers = new _seedcord_core_node_internal.SubscriberLoader(this.bus, config.subscribers.path);
3615
3102
  this.bot = new Bot(this);
3616
3103
  this.rateLimiter = config.store ?? new _seedcord_rate_limiter.MemoryRateLimiter();
3617
- this.healthCheck = new _seedcord_core_node.HealthCheck(this.shutdown, config.healthCheck);
3104
+ this.healthCheck = _seedcord_core_node_internal.HealthCheck.fromOption(this.shutdown, config.healthCheck);
3618
3105
  this.registerStartupTasks();
3619
3106
  }
3620
3107
  /** The bot's discord username, populated after login. */
3621
3108
  get username() {
3622
3109
  return this.bot.client.user?.username;
3623
3110
  }
3111
+ pluginCapabilities() {
3112
+ return {
3113
+ client: this.bot.client,
3114
+ token: this.bot.botToken,
3115
+ rest: this.bot.client.rest
3116
+ };
3117
+ }
3624
3118
  static reset() {
3625
- Seedcord.isInstantiated = false;
3119
+ super.reset();
3626
3120
  _seedcord_logger.LoggerChannelRegistry.instance.reset();
3627
3121
  }
3628
3122
  registerStartupTasks() {
3629
3123
  if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) this.registerHmrAwareModules();
3630
- this.startup.addTask(_seedcord_core_node.StartupPhase.Configuration, "Bus Initialization", async () => {
3124
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "bus-initialization", async () => {
3631
3125
  this.bus.logger.utils.initialization("Subscribers", "start");
3632
- await this.bus.init();
3126
+ await this.subscribers.init();
3633
3127
  this.bus.logger.utils.initialization("Subscribers", "end");
3634
3128
  });
3635
- this.startup.addTask(_seedcord_core_node.StartupPhase.Instantiation, "Bot Initialization", async () => {
3129
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Login, "bot-initialization", async () => {
3636
3130
  this.bot.logger.utils.initialization("Bot", "start");
3637
3131
  await this.bot.init();
3638
3132
  this.bot.logger.utils.initialization("Bot", "end");
3639
3133
  });
3640
- this.startup.addTask(_seedcord_core_node.StartupPhase.Ready, "Health Check", async () => {
3641
- this.healthCheck.logger.utils.initialization("HealthCheck", "start");
3642
- await this.healthCheck.init();
3643
- this.healthCheck.logger.utils.initialization("HealthCheck", "end");
3134
+ const { healthCheck } = this;
3135
+ if (healthCheck) this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Ready, "health-check", async () => {
3136
+ healthCheck.logger.utils.initialization("HealthCheck", "start");
3137
+ await healthCheck.init();
3138
+ healthCheck.logger.utils.initialization("HealthCheck", "end");
3644
3139
  });
3645
3140
  }
3646
3141
  /**
3647
3142
  * Starts the bot and runs all initialization tasks
3648
3143
  *
3649
- * @returns This Seedcord instance when fully initialized
3144
+ * @returns This Seedcord instance when initialized
3650
3145
  */
3651
3146
  async start() {
3652
- await super.init();
3147
+ try {
3148
+ await super.init();
3149
+ } catch (caught) {
3150
+ await this.shutdown.run(1, false);
3151
+ Seedcord.reset();
3152
+ throw caught;
3153
+ }
3653
3154
  return this;
3654
3155
  }
3655
3156
  registerHmrAwareModules() {
3656
- this.startup.addTask(_seedcord_core_node.StartupPhase.Configuration, "HMR Registration", async () => {
3157
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "hmr-registration", async () => {
3657
3158
  this.hmrManager.register(this.bot);
3658
- this.hmrManager.register(this.bus);
3159
+ this.hmrManager.register(this.subscribers);
3659
3160
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
3660
3161
  await Promise.resolve();
3661
3162
  });
@@ -3681,29 +3182,14 @@ exports.MiddlewareType = MiddlewareType;
3681
3182
  exports.ModalHandler = ModalHandler;
3682
3183
  exports.Nsfw = Nsfw;
3683
3184
  exports.Paginator = Paginator;
3684
- exports.Pluggable = Pluggable;
3685
3185
  exports.Plugin = Plugin;
3686
3186
  exports.RegisterEvent = RegisterEvent;
3687
3187
  exports.RepliableHandler = RepliableHandler;
3688
3188
  exports.Seedcord = Seedcord;
3689
3189
  exports.SelectMenuHandler = SelectMenuHandler;
3690
- Object.defineProperty(exports, 'ShutdownPhase', {
3691
- enumerable: true,
3692
- get: function () {
3693
- return _seedcord_core_node.ShutdownPhase;
3694
- }
3695
- });
3696
3190
  exports.SlashHandler = SlashHandler;
3697
- Object.defineProperty(exports, 'StartupPhase', {
3698
- enumerable: true,
3699
- get: function () {
3700
- return _seedcord_core_node.StartupPhase;
3701
- }
3702
- });
3703
- exports.Subscribe = Subscribe;
3704
3191
  exports.Subscriber = Subscriber;
3705
3192
  exports.WebhookLog = WebhookLog;
3706
- exports.WebhookUrl = WebhookUrl;
3707
3193
  exports.checkBotPermissions = checkBotPermissions;
3708
3194
  exports.checkPermissions = checkPermissions;
3709
3195
  exports.fetchGuildMember = fetchGuildMember;
@@ -3731,6 +3217,7 @@ Object.keys(_seedcord_errors).forEach(function (k) {
3731
3217
  });
3732
3218
  });
3733
3219
 
3220
+ var _seedcord_event_emitter = require("@seedcord/event-emitter");
3734
3221
  Object.keys(_seedcord_event_emitter).forEach(function (k) {
3735
3222
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
3736
3223
  enumerable: true,