@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.mjs CHANGED
@@ -1,26 +1,24 @@
1
1
  import "reflect-metadata";
2
2
  import { installNodeDefaults } from "@seedcord/logger/node";
3
- import { BaseReplySender, CommandMetadataKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, SubscribeMetadataKey, WebhookUrlMetadataKey, areRoutes, decodeComponentRoute, deferFlags, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
3
+ import { BaseReplySender, CommandMetadataKey, EventMetadataKey, FaultThrottle, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, PublishDefault, areRoutes, asError, decodeComponentRoute, deferFlags, dispatchedPayload, getDevChannel, outcomeFor, pageCursor, pickNotice, prefixOf, queuedMsFor, reportedWrite, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
4
4
  import { SeedcordErrorCode } from "@seedcord/errors";
5
5
  import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
6
- import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
6
+ import { BaseHandler, BuilderComponent, Bus, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, defineGate, paginate } from "@seedcord/core";
7
7
  import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
8
8
  import { ActionRowBuilder, ButtonBuilder, ContextMenuCommandBuilder, SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
9
9
  import { buildSlashRoute, routeLeavesOf } from "@seedcord/utils/internal";
10
10
  import { ApplicationCommandType, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, TextChannel, User, chatInputApplicationCommandMention } from "discord.js";
11
11
  import * as crypto from "node:crypto";
12
- import { TypedEventEmitter } from "@seedcord/event-emitter";
13
- import chalk from "chalk";
14
- import { Converters, Envapter } from "envapt";
15
- import { MessageFlags as MessageFlags$1, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
16
- import { filterCirculars, formatFilePath, hasKeys, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
17
- import { DiscordAPIError as DiscordAPIError$1, REST } from "@discordjs/rest";
18
- import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, ShutdownPhase as ShutdownPhase$1, StartupPhase, StartupPhase as StartupPhase$1 } from "@seedcord/core/node";
12
+ import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
13
+ import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, SubscriberLoader, settleWithin } from "@seedcord/core/node/internal";
19
14
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
20
15
  import { SeedcordBrand } from "@seedcord/types/internal";
16
+ import { Envapter } from "envapt";
17
+ import chalk from "chalk";
21
18
  import { Envapt } from "envapt/legacy";
22
19
  import { resolve } from "node:path";
23
20
  import { HmrModuleHandler } from "@seedcord/core/hmr";
21
+ import { formatFilePath, hasKeys } from "@seedcord/utils";
24
22
  import { traverseDirectory } from "@seedcord/utils/node";
25
23
 
26
24
  export * from "@seedcord/errors"
@@ -29,11 +27,11 @@ export * from "@seedcord/core"
29
27
 
30
28
  export * from "@seedcord/logger"
31
29
 
32
- export * from "@seedcord/event-emitter"
30
+ export * from "@seedcord/rate-limiter"
33
31
 
34
32
  export * from "@seedcord/utils"
35
33
 
36
- export * from "@seedcord/rate-limiter"
34
+ export * from "@seedcord/event-emitter"
37
35
 
38
36
  //#region src/logger-bootstrap.ts
39
37
  installNodeDefaults();
@@ -147,7 +145,7 @@ let MiddlewareType = /* @__PURE__ */ function(MiddlewareType) {
147
145
  * Interaction middleware cannot specify event filters.
148
146
  *
149
147
  * @param type - Middleware kind from {@link MiddlewareType}
150
- * @param priority - Ordering value where lower runs earlier. {@default `0`}
148
+ * @param priority - Ordering value where lower runs earlier. {@default `0` }
151
149
  * @param options - Additional registration options
152
150
  *
153
151
  * @decorator
@@ -335,7 +333,7 @@ const Emojis = emojiStorage;
335
333
  */
336
334
  var EmojiInjector = class {
337
335
  core;
338
- logger = new Logger("Emojis");
336
+ logger = new Logger("Emojis", { channel: "bot" });
339
337
  constructor(core) {
340
338
  this.core = core;
341
339
  }
@@ -410,7 +408,7 @@ const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
410
408
  */
411
409
  var CommandMentionInjector = class {
412
410
  core;
413
- logger = new Logger("CommandMentions");
411
+ logger = new Logger("CommandMentions", { channel: "bot" });
414
412
  constructor(core) {
415
413
  this.core = core;
416
414
  }
@@ -568,7 +566,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
568
566
  }
569
567
  const me = target.guild.members.me;
570
568
  if (!me) {
571
- new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
569
+ new Logger("checkBotPermissions", { channel: "errors" }).warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
572
570
  const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
573
571
  throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
574
572
  }
@@ -767,8 +765,11 @@ function seedState(interaction) {
767
765
  */
768
766
  var ReplySender = class extends BaseReplySender {
769
767
  interaction;
770
- constructor(interaction, routeId) {
771
- super(routeId, seedState(interaction));
768
+ constructor(interaction, routeId, bus) {
769
+ super(routeId, seedState(interaction), bus && {
770
+ bus,
771
+ interactionId: interaction.id
772
+ });
772
773
  this.interaction = interaction;
773
774
  }
774
775
  async writeReply(response, opts) {
@@ -875,43 +876,9 @@ function slashRouteOf(interaction) {
875
876
  return buildSlashRoute(interaction.commandName, subcommand, group);
876
877
  }
877
878
 
878
- //#endregion
879
- //#region src/miscellaneous/FaultThrottle.ts
880
- /**
881
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
882
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
883
- * the map stays small, and a stale entry is evicted on the next read of its key.
884
- *
885
- * @internal
886
- */
887
- var FaultThrottle = class {
888
- windowMs;
889
- now;
890
- lastReportedAt = /* @__PURE__ */ new Map();
891
- constructor(windowMs = 6e4, now = () => Date.now()) {
892
- this.windowMs = windowMs;
893
- this.now = now;
894
- }
895
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
896
- shouldReport(key) {
897
- const last = this.lastReportedAt.get(key);
898
- if (last === void 0) return true;
899
- if (this.now() - last < this.windowMs) return false;
900
- this.lastReportedAt.delete(key);
901
- return true;
902
- }
903
- markReported(key) {
904
- this.lastReportedAt.set(key, this.now());
905
- }
906
- clear() {
907
- this.lastReportedAt.clear();
908
- }
909
- };
910
-
911
879
  //#endregion
912
880
  //#region src/miscellaneous/extractErrorResponse.ts
913
- const logger$2 = new Logger("ErrorsHandling");
914
- const faultThrottle = new FaultThrottle();
881
+ const logger$2 = new Logger("ErrorsHandling", { channel: "errors" });
915
882
  /**
916
883
  * Processes an error into a user-facing reply and routes its report.
917
884
  *
@@ -941,29 +908,30 @@ function extractErrorResponse(error, core, origin) {
941
908
  response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
942
909
  };
943
910
  }
944
- function withThrottle(origin, error, publish) {
911
+ function withThrottle(core, origin, error, publish) {
912
+ const throttle = FaultThrottle.for(core);
945
913
  const key = faultKey(origin, error);
946
- if (!faultThrottle.shouldReport(key)) {
947
- logger$2.info(`throttled duplicate fault ${key}`);
914
+ if (!throttle.shouldReport(key)) {
915
+ logger$2.debug(`throttled duplicate fault ${key}`);
948
916
  return;
949
917
  }
950
918
  publish();
951
- faultThrottle.markReported(key);
919
+ throttle.markReported(key);
952
920
  }
953
921
  function reportFault(denial, core, origin, uuid) {
954
- withThrottle(origin, denial, () => {
955
- logger$2.error(`${denial.name}: ${uuid}`, denial);
956
- if (origin.interaction) core.bus.publish("handledException", {
922
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
923
+ withThrottle(core, origin, denial, () => {
924
+ if (origin.interaction) core.bus[PublishDefault]("handledException", {
957
925
  denial,
958
926
  uuid,
959
927
  source: buildInteractionSource(origin.interaction)
960
928
  });
961
- else if (origin.event) core.bus.publish("handledException", {
929
+ else if (origin.event) core.bus[PublishDefault]("handledException", {
962
930
  denial,
963
931
  uuid,
964
932
  source: buildEventSource(origin.event, origin)
965
933
  });
966
- else core.bus.publish("unknownException", {
934
+ else core.bus[PublishDefault]("unknownException", {
967
935
  uuid,
968
936
  error: denial,
969
937
  ...scalarActors(origin),
@@ -978,10 +946,10 @@ function causeLine(error) {
978
946
  return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
979
947
  }
980
948
  function reportRawFault(error, core, origin, uuid) {
981
- withThrottle(origin, error, () => {
982
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
983
- else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
984
- core.bus.publish("unknownException", {
949
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
950
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
951
+ withThrottle(core, origin, error, () => {
952
+ core.bus[PublishDefault]("unknownException", {
985
953
  uuid,
986
954
  error,
987
955
  ...scalarActors(origin),
@@ -1124,112 +1092,6 @@ async function getConfirmation(interaction, prompt, options) {
1124
1092
  return confirmed;
1125
1093
  }
1126
1094
 
1127
- //#endregion
1128
- //#region src/interfaces/Plugin.ts
1129
- /**
1130
- * Base class for Seedcord plugins
1131
- *
1132
- * Extend this class to create plugins that integrate with the Seedcord lifecycle.
1133
- * Plugins have access to the core instance and must implement initialization logic.
1134
- */
1135
- var Plugin = class extends TypedEventEmitter {
1136
- pluggable;
1137
- name = this.constructor.name;
1138
- constructor(pluggable) {
1139
- super();
1140
- this.pluggable = pluggable;
1141
- }
1142
- /**
1143
- * Reloads plugin state on an HMR update. The base implementation is a no-op, override it in your plugin.
1144
- * @virtual
1145
- */
1146
- onHmr(_event) {
1147
- return Promise.resolve();
1148
- }
1149
- /**
1150
- * Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
1151
- * @param patterns - Glob patterns relative to the project root
1152
- */
1153
- registerCriticalFiles(patterns) {
1154
- getDevChannel()?.send("seedcord:register-critical-files", { patterns });
1155
- }
1156
- /** @internal */
1157
- removeListener(event, listener) {
1158
- return super.removeListener(event, listener);
1159
- }
1160
- /** @internal */
1161
- removeAllListeners(event) {
1162
- return super.removeAllListeners(event);
1163
- }
1164
- };
1165
- /**
1166
- * Base class for objects that can have plugins attached.
1167
- *
1168
- * Provides plugin attachment capabilities and lifecycle management. Plugins are attached during
1169
- * configuration and initialized during startup. Not constructed directly, the host is a {@link Seedcord}.
1170
- */
1171
- var Pluggable = class Pluggable extends TypedEventEmitter {
1172
- isInitialized = false;
1173
- shutdown;
1174
- startup;
1175
- plugins = [];
1176
- static PLUGIN_INIT_TIMEOUT_MS = 15e3;
1177
- constructor(shutdown, startup) {
1178
- super();
1179
- this.shutdown = shutdown;
1180
- this.startup = startup;
1181
- }
1182
- /** @internal */
1183
- async init() {
1184
- if (this.isInitialized) return this;
1185
- await this.startup.run();
1186
- this.isInitialized = true;
1187
- return this;
1188
- }
1189
- /**
1190
- * Attaches a plugin to this instance
1191
- *
1192
- * Plugins provide external functionality and are initialized during the specified startup phase.
1193
- * The plugin instance becomes available as a property in `core` wherever it's available.
1194
- *
1195
- * Make sure to augment the {@link Core} interface with the plugin type to ensure TypeScript recognizes it and provides intellisense.
1196
- *
1197
- * @typeParam Key - The property name for accessing the plugin
1198
- * @typeParam Ctor - The plugin constructor type
1199
- * @param key - Property name to access the plugin instance
1200
- * @param Plugin - Plugin constructor class
1201
- * @param startupPhase - When during startup to initialize this plugin ({@link StartupPhase})
1202
- * @param args - Additional arguments to pass to the plugin constructor
1203
- * @returns This instance with the plugin attached as a typed property
1204
- * @throws A **SeedcordError** When called after initialization or if key already exists
1205
- * @example
1206
- * ```typescript
1207
- * seedcord.attach('db', Mongo, StartupPhase.Configuration, { uri: 'mongodb://...', name: 'seedcord', dir: ... })
1208
- * ```
1209
- */
1210
- attach(key, Plugin, startupPhase, ...args) {
1211
- if (this.isInitialized) throw new SeedcordError(SeedcordErrorCode.CorePluginAfterInit);
1212
- if (key in this) throw new SeedcordError(SeedcordErrorCode.CorePluginKeyExists, [key]);
1213
- const instance = new Plugin(this, ...args);
1214
- this.plugins.push(instance);
1215
- const entry = { [key]: instance };
1216
- this.startup.addTask(startupPhase, `Plugin:${key}`, async () => {
1217
- instance.logger.utils.initialization(key, "start");
1218
- await instance.init();
1219
- instance.logger.utils.initialization(key, "end");
1220
- }, Pluggable.PLUGIN_INIT_TIMEOUT_MS);
1221
- return Object.assign(this, entry);
1222
- }
1223
- /** @internal */
1224
- removeListener(event, listener) {
1225
- return super.removeListener(event, listener);
1226
- }
1227
- /** @internal */
1228
- removeAllListeners(event) {
1229
- return super.removeAllListeners(event);
1230
- }
1231
- };
1232
-
1233
1095
  //#endregion
1234
1096
  //#region src/handlers/BaseHandler.ts
1235
1097
  /**
@@ -1255,7 +1117,7 @@ var BaseHandler$1 = class extends BaseHandler {
1255
1117
  */
1256
1118
  var InteractionMiddleware = class extends BaseHandler$1 {
1257
1119
  constructor(event, core) {
1258
- super(event, core);
1120
+ super(event, core, void 0, "interactions");
1259
1121
  }
1260
1122
  };
1261
1123
 
@@ -1275,8 +1137,8 @@ var RepliableHandler = class extends RepliableHandler$1 {
1275
1137
  getEvent() {
1276
1138
  return this.event;
1277
1139
  }
1278
- buildSender(event, _core, routeId) {
1279
- return new ReplySender(event, routeId);
1140
+ buildSender(event, core, routeId) {
1141
+ return new ReplySender(event, routeId, core.bus);
1280
1142
  }
1281
1143
  };
1282
1144
 
@@ -1431,7 +1293,7 @@ var ContextMenuHandler = class extends InteractionHandler {
1431
1293
  */
1432
1294
  var AutocompleteHandler = class extends BaseHandler$1 {
1433
1295
  constructor(event, core, dispatch) {
1434
- super(event, core, dispatch);
1296
+ super(event, core, dispatch, "interactions");
1435
1297
  }
1436
1298
  decodedFocused;
1437
1299
  /**
@@ -1482,7 +1344,11 @@ var AutocompleteHandler = class extends BaseHandler$1 {
1482
1344
  }
1483
1345
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1484
1346
  async respond(choices) {
1485
- await this.event.respond(choices);
1347
+ const routeId = this.dispatch?.routeId ?? "autocomplete";
1348
+ await reportedWrite({
1349
+ bus: this.core.bus,
1350
+ interactionId: this.event.id
1351
+ }, routeId, "respond", () => this.event.respond(choices));
1486
1352
  }
1487
1353
  /** The firing command route, for a field whose completion differs per registered command. */
1488
1354
  get route() {
@@ -1675,7 +1541,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
1675
1541
  var EventHandler = class extends BaseHandler$1 {
1676
1542
  firedEvent;
1677
1543
  constructor(event, core, eventName) {
1678
- super(event, core);
1544
+ super(event, core, void 0, "events");
1679
1545
  this.firedEvent = eventName;
1680
1546
  }
1681
1547
  /**
@@ -1728,7 +1594,7 @@ var EventHandler = class extends BaseHandler$1 {
1728
1594
  var EventMiddleware = class extends BaseHandler$1 {
1729
1595
  firedEvent;
1730
1596
  constructor(event, core, eventName) {
1731
- super(event, core);
1597
+ super(event, core, void 0, "events");
1732
1598
  this.firedEvent = eventName;
1733
1599
  }
1734
1600
  /**
@@ -1913,10 +1779,15 @@ var Paginator = class {
1913
1779
  }
1914
1780
  };
1915
1781
  }
1916
- /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
1917
- async start(interaction, core) {
1918
- const response = await this.page(contextOf(interaction, core), 0);
1919
- return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
1782
+ /**
1783
+ * Render page 0 and send it through the handler's sender, picking reply or followUp from its ack state.
1784
+ *
1785
+ * @param handler - The handler starting the paginator, normally `this`.
1786
+ */
1787
+ async start(handler) {
1788
+ const interaction = handler.getEvent();
1789
+ const response = await this.page(contextOf(interaction, handler.core), 0);
1790
+ return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1920
1791
  }
1921
1792
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
1922
1793
  async page(ctx, n) {
@@ -1976,250 +1847,32 @@ var CursorSource = class {
1976
1847
  };
1977
1848
 
1978
1849
  //#endregion
1979
- //#region src/subscribers/Subscriber.ts
1980
- /**
1981
- * Abstract base class for handling application subscribers.
1982
- *
1983
- * Provides type-safe access to subscribers data and the core framework instance.
1984
- * Extend this class to create custom subscriber handlers.
1985
- *
1986
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
1987
- */
1988
- var Subscriber = class {
1989
- data;
1990
- core;
1991
- logger;
1992
- /**
1993
- * Instantiates the subscriber with event data and core.
1994
- *
1995
- * @param data - The subscriber event data
1996
- * @param core - The core framework instance
1997
- */
1998
- constructor(data, core) {
1999
- this.data = data;
2000
- this.core = core;
2001
- this.logger = new Logger(this.constructor.name);
2002
- }
2003
- };
2004
-
2005
- //#endregion
2006
- //#region src/subscribers/decorators/Subscribe.ts
1850
+ //#region src/subscribers/index.ts
2007
1851
  /**
2008
- * Registers a subscriber handler class with a specific subscriber event.
2009
- *
2010
- * Associates the decorated class with a subscriber event type for automatic
2011
- * registration and execution when the subscriber is emitted.
1852
+ * Base class for a gateway subscriber, binding `this.core` to the gateway {@link Core}.
2012
1853
  *
2013
- * @typeParam TSubscriber - The subscriber event name to register for
2014
- * @param subscriber - The subscriber event name to register for
2015
- * @param options - Options to configure the subscriber handler registration.
2016
- * @decorator
2017
- * @example
2018
- * ```typescript
2019
- * \@Subscribe('userJoin')
2020
- * class WelcomeHandler extends Subscriber<'userJoin'> {
2021
- * async execute() {
2022
- * // Handle user join event
2023
- * }
2024
- * }
2025
- * ```
2026
- * @example
2027
- * ```ts
2028
- * \@Subscribe('userJoin', { frequency: 'once' })
2029
- * // or
2030
- * \@Subscribe('userJoin')
2031
- * ```
1854
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
2032
1855
  */
2033
- function Subscribe(subscriber, options) {
2034
- return function(constructor) {
2035
- const meta = {
2036
- subscriber,
2037
- frequency: options?.frequency
2038
- };
2039
- Reflect.defineMetadata(SubscribeMetadataKey, meta, constructor);
2040
- };
2041
- }
2042
-
2043
- //#endregion
2044
- //#region src/subscribers/decorators/WebhookUrl.ts
2045
- /**
2046
- * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
2047
- *
2048
- * An unset variable disables the reporter with a boot warning. A set but malformed value throws
2049
- * at boot.
2050
- *
2051
- * @param envKey - The environment variable name holding the webhook url
2052
- * @decorator
2053
- * @example
2054
- * ```ts
2055
- * \@Subscribe('memberBanned')
2056
- * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
2057
- * class BanLog extends WebhookLog<'memberBanned'> {
2058
- * report() {
2059
- * return { components: [new BanCard(this.data).component] };
2060
- * }
2061
- * }
2062
- * ```
2063
- */
2064
- function WebhookUrl(envKey) {
2065
- return function(constructor) {
2066
- Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
2067
- };
2068
- }
2069
-
2070
- //#endregion
2071
- //#region src/miscellaneous/flagsFor.ts
2072
- /** @internal */
2073
- function flagsFor(ephemeral) {
2074
- let flags = MessageFlags$1.IsComponentsV2;
2075
- if (ephemeral) flags |= MessageFlags$1.Ephemeral;
2076
- return flags;
2077
- }
2078
-
2079
- //#endregion
2080
- //#region src/subscribers/bases/webhookHelpers.ts
2081
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
2082
- function isDiscordWebhookUrl(value) {
2083
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
2084
- }
2085
- function jsonAttachment(name, description, data) {
2086
- const content = filterCirculars(data);
2087
- return {
2088
- name,
2089
- description,
2090
- data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
2091
- };
2092
- }
2093
- function neutralizeFences(text) {
2094
- return text.replaceAll("```", "`​`​`");
2095
- }
2096
- /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
2097
- function errorReport(error) {
2098
- let report = error.stack ?? `${error.name}: ${error.message}`;
2099
- if (Error.isError(error.cause)) {
2100
- const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
2101
- report += `\n\nCaused by:\n${cause}`;
2102
- }
2103
- const stripped = neutralizeFences(stripAnsi(report));
2104
- return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
2105
- }
2106
- var WebhookSeparator = class extends BuilderComponent {
2107
- constructor() {
2108
- super("separator");
2109
- this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
2110
- }
2111
- };
2112
-
2113
- //#endregion
2114
- //#region src/subscribers/bases/WebhookSender.ts
2115
- const HTTP_NOT_FOUND = 404;
2116
- const HTTP_UNAUTHORIZED = 401;
1856
+ var Subscriber = class extends Subscriber$1 {};
2117
1857
  /**
2118
- * Sends component messages to a Discord webhook through `@discordjs/rest`.
1858
+ * Base class for a gateway subscriber that delivers its event to a Discord webhook, binding
1859
+ * `this.core` to the gateway {@link Core}.
2119
1860
  *
2120
- * @throws A `SeedcordError` when the url does not carry a webhook id and token.
1861
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2121
1862
  */
2122
- var WebhookSender = class {
2123
- rest = new REST();
2124
- route;
2125
- constructor(url) {
2126
- const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2127
- if (!match?.[1] || !match[2]) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2128
- this.route = Routes.webhook(match[1], match[2]);
2129
- }
2130
- /**
2131
- * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2132
- * Discord and `unreachable` on any other failure.
2133
- */
2134
- async verify() {
2135
- try {
2136
- await this.rest.get(this.route, { auth: false });
2137
- return "ok";
2138
- } catch (error) {
2139
- return error instanceof DiscordAPIError$1 && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2140
- }
2141
- }
2142
- async send(options) {
2143
- const { flags, username, components, files } = options;
2144
- await this.rest.post(this.route, {
2145
- auth: false,
2146
- query: new URLSearchParams({
2147
- with_components: "true",
2148
- wait: "true"
2149
- }),
2150
- body: {
2151
- flags,
2152
- username,
2153
- components: components.map((component) => component.toJSON()),
2154
- ...files?.length && { attachments: files.map((file, id) => ({
2155
- id,
2156
- filename: file.name,
2157
- description: file.description
2158
- })) }
2159
- },
2160
- ...files?.length && { files: files.map((file) => ({
2161
- name: file.name,
2162
- data: file.data
2163
- })) }
2164
- });
2165
- }
2166
- };
1863
+ var WebhookLog = class extends WebhookLog$1 {};
2167
1864
 
2168
1865
  //#endregion
2169
- //#region src/subscribers/bases/WebhookLog.ts
1866
+ //#region src/plugin.ts
2170
1867
  /**
2171
- * Base class for subscribers that deliver their event to a Discord webhook.
1868
+ * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
2172
1869
  *
2173
- * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2174
- * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2175
- * the reporter is skipped at registration with a boot warning.
1870
+ * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
1871
+ * `this.core` carries the shared members only.
2176
1872
  *
2177
- * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1873
+ * @typeParam Opts - The transport, runtime, and capabilities the plugin declares.
2178
1874
  */
2179
- var WebhookLog = class WebhookLog extends Subscriber {
2180
- static senders = /* @__PURE__ */ new Map();
2181
- async execute() {
2182
- const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2183
- if (url === null) {
2184
- this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2185
- return;
2186
- }
2187
- const { username, components, files } = await this.report();
2188
- try {
2189
- await WebhookLog.senderFor(url).send({
2190
- flags: flagsFor(false),
2191
- username: username ?? this.constructor.name,
2192
- components,
2193
- files
2194
- });
2195
- } catch (error) {
2196
- this.logger.error("Failed to send webhook report", error);
2197
- }
2198
- }
2199
- /** @internal */
2200
- static senderFor(url) {
2201
- let sender = WebhookLog.senders.get(url);
2202
- if (!sender) {
2203
- sender = new WebhookSender(url);
2204
- WebhookLog.senders.set(url, sender);
2205
- }
2206
- return sender;
2207
- }
2208
- /** @internal */
2209
- static envKeyOf(ctor) {
2210
- const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
2211
- if (!envKey) throw new SeedcordError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2212
- return envKey;
2213
- }
2214
- /** @internal Returns null when the env var is unset, throws when set but malformed. */
2215
- static urlOf(envKey) {
2216
- if (!Envapter.has(envKey)) return null;
2217
- const raw = Envapter.getRequired(envKey, Converters.String).trim();
2218
- if (raw === "") return null;
2219
- if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2220
- return raw;
2221
- }
2222
- };
1875
+ var Plugin = class extends Plugin$1 {};
2223
1876
 
2224
1877
  //#endregion
2225
1878
  //#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
@@ -2275,8 +1928,7 @@ function slashRouteLeaves(commands) {
2275
1928
  var CommandRegistry = class {
2276
1929
  core;
2277
1930
  onDeployed;
2278
- name = "Commands";
2279
- logger = new Logger("Commands");
1931
+ logger = new Logger("Commands", { channel: "commands" });
2280
1932
  isInitialised = false;
2281
1933
  globalCommands = [];
2282
1934
  guildCommands = new Collection();
@@ -2363,7 +2015,7 @@ var CommandRegistry = class {
2363
2015
  this.registerCommand(exported, rel);
2364
2016
  this.hmrHandler?.trackHandler(fullPath, exported);
2365
2017
  }
2366
- }, this.logger);
2018
+ });
2367
2019
  }
2368
2020
  isCommandClass(obj) {
2369
2021
  if (typeof obj !== "function") return false;
@@ -2525,7 +2177,7 @@ function eventGateContext(eventName, args, core) {
2525
2177
 
2526
2178
  //#endregion
2527
2179
  //#region src/bot/handleEventFault.ts
2528
- const logger$1 = new Logger("EventBoundary");
2180
+ const logger$1 = new Logger("EventBoundary", { channel: "errors" });
2529
2181
  /**
2530
2182
  * The event controller boundary. Reports a throw from an event handler and never replies, since a
2531
2183
  * generic event has no reply target. A {@link Silence} or a non-reporting {@link Notice} stops quietly, a
@@ -2538,15 +2190,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2538
2190
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2539
2191
  return;
2540
2192
  }
2541
- if (!Error.isError(caught)) throw caught;
2193
+ const error = asError(caught);
2542
2194
  const ignore = new Set(core.config.errors?.ignoreEventApiCodes ?? []);
2543
- if (caught instanceof DiscordAPIError && ignore.has(caught.code)) {
2544
- logger$1.debug(`swallowed api code ${caught.code}`);
2195
+ if (error instanceof DiscordAPIError && ignore.has(error.code)) {
2196
+ logger$1.debug(`swallowed api code ${error.code}`);
2545
2197
  return;
2546
2198
  }
2547
- if (caught instanceof Notice && !caught.report) return;
2199
+ if (error instanceof Notice && !error.report) return;
2548
2200
  const actor = deriveEventActor(args);
2549
- extractErrorResponse(caught, core, {
2201
+ extractErrorResponse(error, core, {
2550
2202
  event: {
2551
2203
  name: eventName,
2552
2204
  handler: handlerName,
@@ -2561,21 +2213,22 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2561
2213
  //#endregion
2562
2214
  //#region src/bot/controllers/EventDispatcher.ts
2563
2215
  /**
2564
- * Scans event handler directories, registers handlers with Discord client events,
2565
- * and coordinates event execution through the handler system. Does not handle interactions.
2566
- * Multiple handlers can point to one event.
2216
+ * Scans event handler directories, attaches one client listener per registered event, and runs the
2217
+ * handlers for each fire. Interactions route through InteractionDispatcher. Multiple handlers can
2218
+ * point to one event.
2567
2219
  *
2568
2220
  * @internal
2569
2221
  */
2570
2222
  var EventDispatcher = class {
2571
2223
  core;
2572
- logger = new Logger("Events");
2224
+ logger = new Logger("Events", { channel: "events" });
2573
2225
  isInitialized = false;
2574
- name = "Events";
2575
2226
  eventMap = /* @__PURE__ */ new Map();
2576
2227
  middlewares = [];
2577
2228
  executedOnceHandlers = /* @__PURE__ */ new Set();
2578
2229
  attachedEvents = /* @__PURE__ */ new Set();
2230
+ inFlight = /* @__PURE__ */ new Set();
2231
+ draining = false;
2579
2232
  loading = false;
2580
2233
  loadedHandlers = [];
2581
2234
  loadedMiddlewares = [];
@@ -2639,7 +2292,7 @@ var EventDispatcher = class {
2639
2292
  this.registerHandler(val, relativePath);
2640
2293
  this.hmrHandler?.trackHandler(fullPath, val);
2641
2294
  }
2642
- }, this.logger);
2295
+ });
2643
2296
  }
2644
2297
  async loadMiddlewares(dir) {
2645
2298
  await traverseDirectory(dir, (fullPath, relativePath, imported) => {
@@ -2648,7 +2301,7 @@ var EventDispatcher = class {
2648
2301
  this.registerMiddleware(val, relativePath);
2649
2302
  this.hmrHandler?.trackMiddleware(fullPath, val);
2650
2303
  }
2651
- }, this.logger);
2304
+ });
2652
2305
  }
2653
2306
  /** @internal For use in dev mode */
2654
2307
  async onHmr(event) {
@@ -2744,15 +2397,26 @@ var EventDispatcher = class {
2744
2397
  const handlerEntries = this.eventMap.get(eventName);
2745
2398
  this.logger.debug(`Attaching ${paint.mint.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2746
2399
  this.core.bot.client.on(eventName, (...args) => {
2747
- this.core.bot.emitSafe("any:event", eventName, ...args);
2748
- (async () => {
2749
- await this.processEvent(eventName, args).catch((err) => {
2750
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2751
- this.core.bot.emit("error:unhandled:event", err);
2752
- });
2753
- })();
2400
+ if (this.draining) return;
2401
+ this.core.bus[PublishDefault]("anyEvent", {
2402
+ name: eventName,
2403
+ args
2404
+ });
2405
+ const run = this.processEvent(eventName, args).catch((caught) => {
2406
+ const error = asError(caught);
2407
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2408
+ this.core.bus[PublishDefault]("unhandledEventError", { error });
2409
+ });
2410
+ this.inFlight.add(run);
2411
+ run.finally(() => this.inFlight.delete(run));
2754
2412
  });
2755
2413
  }
2414
+ stopAccepting() {
2415
+ this.draining = true;
2416
+ }
2417
+ drain(timeoutMs) {
2418
+ return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
2419
+ }
2756
2420
  async processEvent(eventName, args) {
2757
2421
  const handlerEntries = this.eventMap.get(eventName);
2758
2422
  if (!handlerEntries || handlerEntries.length === 0) return;
@@ -2817,7 +2481,7 @@ const HARMLESS_API_CODES = new Set([
2817
2481
 
2818
2482
  //#endregion
2819
2483
  //#region src/bot/handleInteractionFault.ts
2820
- const logger = new Logger("InteractionBoundary");
2484
+ const logger = new Logger("InteractionBoundary", { channel: "errors" });
2821
2485
  /**
2822
2486
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
2823
2487
  * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
@@ -2826,33 +2490,45 @@ const logger = new Logger("InteractionBoundary");
2826
2490
  *
2827
2491
  * @internal
2828
2492
  */
2829
- async function handleInteractionFault(caught, interaction, core, sender) {
2493
+ async function handleInteractionFault(caught, interaction, core, routeId, sender) {
2830
2494
  if (caught instanceof Silence) {
2831
2495
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2832
2496
  return;
2833
2497
  }
2834
- if (!Error.isError(caught)) throw caught;
2498
+ const error = asError(caught);
2835
2499
  const ignore = new Set(core.config.errors?.ignoreApiCodes ?? []);
2836
- if (caught instanceof DiscordAPIError && ignore.has(caught.code)) {
2837
- logger.debug(`swallowed api code ${caught.code}`);
2500
+ if (error instanceof DiscordAPIError && ignore.has(error.code)) {
2501
+ logger.debug(`swallowed api code ${error.code}`);
2838
2502
  return;
2839
2503
  }
2840
2504
  if (interaction.isAutocomplete()) {
2841
- extractErrorResponse(caught, core, {
2505
+ extractErrorResponse(error, core, {
2842
2506
  guild: interaction.guild,
2843
2507
  user: interaction.user,
2844
2508
  metadata: interaction,
2845
2509
  route: slashRouteOf(interaction)
2846
2510
  });
2511
+ await sendEmptyChoices(interaction, core, routeId);
2847
2512
  return;
2848
2513
  }
2849
- const { response } = extractErrorResponse(caught, core, {
2514
+ const { response } = extractErrorResponse(error, core, {
2850
2515
  interaction,
2851
2516
  guild: interaction.guild,
2852
2517
  user: interaction.user,
2853
2518
  metadata: interaction
2854
2519
  });
2855
- await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof Notice ? caught.ephemeral : true);
2520
+ await sendGuarded(sender ?? new ReplySender(interaction, routeId, core.bus), response, error instanceof Notice ? error.ephemeral : true);
2521
+ }
2522
+ async function sendEmptyChoices(interaction, core, routeId) {
2523
+ const telemetry = {
2524
+ bus: core.bus,
2525
+ interactionId: interaction.id
2526
+ };
2527
+ try {
2528
+ await reportedWrite(telemetry, routeId, "respond", () => interaction.respond([]));
2529
+ } catch (error) {
2530
+ logger.debug(`autocomplete empty-choices send failed: ${String(error)}`);
2531
+ }
2856
2532
  }
2857
2533
  async function sendGuarded(sender, response, ephemeral) {
2858
2534
  try {
@@ -2876,9 +2552,8 @@ async function sendGuarded(sender, response, ephemeral) {
2876
2552
  */
2877
2553
  var InteractionDispatcher = class {
2878
2554
  core;
2879
- logger = new Logger("Interactions");
2555
+ logger = new Logger("Interactions", { channel: "interactions" });
2880
2556
  isInitialized = false;
2881
- name = "Interactions";
2882
2557
  slashMap = /* @__PURE__ */ new Map();
2883
2558
  buttonMap = /* @__PURE__ */ new Map();
2884
2559
  modalMap = /* @__PURE__ */ new Map();
@@ -2890,8 +2565,11 @@ var InteractionDispatcher = class {
2890
2565
  messageContextMenuMap = /* @__PURE__ */ new Map();
2891
2566
  userContextMenuMap = /* @__PURE__ */ new Map();
2892
2567
  autocompleteMap = /* @__PURE__ */ new Map();
2568
+ handlerFiles = /* @__PURE__ */ new Map();
2893
2569
  keysToIgnore = /* @__PURE__ */ new Set();
2894
2570
  middlewares = [];
2571
+ inFlight = /* @__PURE__ */ new Set();
2572
+ draining = false;
2895
2573
  loading = false;
2896
2574
  loadedHandlers = [];
2897
2575
  loadedMiddlewares = [];
@@ -3006,7 +2684,7 @@ var InteractionDispatcher = class {
3006
2684
  this.registerHandler(val, relativePath);
3007
2685
  this.hmrHandler?.trackHandler(fullPath, val);
3008
2686
  }
3009
- }, this.logger);
2687
+ });
3010
2688
  }
3011
2689
  async loadMiddlewares(dir) {
3012
2690
  await traverseDirectory(dir, (fullPath, relativePath, imported) => {
@@ -3015,7 +2693,7 @@ var InteractionDispatcher = class {
3015
2693
  this.registerMiddleware(val, relativePath);
3016
2694
  this.hmrHandler?.trackMiddleware(fullPath, val);
3017
2695
  }
3018
- }, this.logger);
2696
+ });
3019
2697
  }
3020
2698
  registerMiddleware(middlewareCtor, relativePath) {
3021
2699
  const metadata = Reflect.getMetadata(MiddlewareMetadataKey, middlewareCtor);
@@ -3045,6 +2723,7 @@ var InteractionDispatcher = class {
3045
2723
  return obj.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(MiddlewareMetadataKey, obj);
3046
2724
  }
3047
2725
  registerHandler(handlerClass, relativePath) {
2726
+ const from = formatFilePath(relativePath);
3048
2727
  const writes = [];
3049
2728
  for (const [routeType, map] of this.routeTypes) {
3050
2729
  const meta = Reflect.getMetadata(InteractionRouteKeys[routeType], handlerClass);
@@ -3052,16 +2731,16 @@ var InteractionDispatcher = class {
3052
2731
  for (const route of meta) {
3053
2732
  const existing = map.get(route);
3054
2733
  if (existing && existing !== handlerClass) throw new SeedcordError(SeedcordErrorCode.InteractionDuplicateRoute, [
3055
- route,
3056
- existing.name,
3057
- handlerClass.name
2734
+ `${routeType}:${route}`,
2735
+ `${existing.name} (${this.handlerFiles.get(existing) ?? "unknown file"})`,
2736
+ `${handlerClass.name} (${from})`
3058
2737
  ]);
3059
2738
  writes.push([map, route]);
3060
2739
  }
3061
2740
  }
3062
2741
  if (writes.length === 0) return;
3063
2742
  for (const [map, route] of writes) map.set(route, handlerClass);
3064
- const from = formatFilePath(relativePath);
2743
+ this.handlerFiles.set(handlerClass, from);
3065
2744
  if (this.loading) this.loadedHandlers.push({
3066
2745
  name: handlerClass.name,
3067
2746
  from
@@ -3073,6 +2752,7 @@ var InteractionDispatcher = class {
3073
2752
  await this.hmrHandler?.handle(event);
3074
2753
  }
3075
2754
  unregisterHandler(handlerClass, artifacts) {
2755
+ this.handlerFiles.delete(handlerClass);
3076
2756
  if (artifacts) {
3077
2757
  for (const { routeType, routes } of artifacts) {
3078
2758
  const map = this.routeTypes.find(([type]) => type === routeType)?.[1];
@@ -3092,44 +2772,92 @@ var InteractionDispatcher = class {
3092
2772
  }
3093
2773
  attachToClient() {
3094
2774
  this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
3095
- this.core.bot.emitSafe("any:interaction", interaction);
3096
- this.handleInteraction(interaction).catch((err) => {
3097
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3098
- this.core.bot.emit("error:unhandled:interaction", err);
2775
+ if (this.draining) return;
2776
+ this.core.bus[PublishDefault]("anyInteraction", { interaction });
2777
+ const run = this.handleInteraction(interaction).catch((caught) => {
2778
+ const error = asError(caught);
2779
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2780
+ this.core.bus[PublishDefault]("unhandledInteractionError", { error });
3099
2781
  });
2782
+ this.inFlight.add(run);
2783
+ run.finally(() => this.inFlight.delete(run));
3100
2784
  });
3101
2785
  }
3102
- async handleCustomIdInteraction(interaction, getMap, interactionType) {
2786
+ stopAccepting() {
2787
+ this.draining = true;
2788
+ }
2789
+ drain(timeoutMs) {
2790
+ return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
2791
+ }
2792
+ async handleCustomIdInteraction(interaction, kind, getMap) {
3103
2793
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
3104
2794
  const prefix = prefixOf(interaction.customId);
3105
- if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3106
- await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
2795
+ if (!prefix) this.logger.warn(`${paint.sky.bold(kind)} has invalid customId: ${paint.mute(interaction.customId)}`);
2796
+ await this.processInteraction(interaction, kind, () => prefix, (key) => getMap().get(key));
3107
2797
  }
3108
- async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
2798
+ async processInteraction(interaction, kind, extractKey, getHandler, fallback = UnhandledRepliable) {
3109
2799
  const key = extractKey(interaction);
2800
+ const startedAt = performance.now();
2801
+ const queuedMs = queuedMsFor(interaction.id);
2802
+ const matched = getHandler(key);
2803
+ let routeId = `${kind}:${key || "unrouted"}`;
2804
+ let reported = false;
2805
+ const report = (outcome) => {
2806
+ if (reported) return;
2807
+ reported = true;
2808
+ this.core.bus[PublishDefault]("interactionDispatched", dispatchedPayload({
2809
+ routeId,
2810
+ interactionId: interaction.id,
2811
+ kind,
2812
+ outcome,
2813
+ startedAt,
2814
+ fallback: !matched,
2815
+ queuedMs
2816
+ }));
2817
+ };
3110
2818
  let sender;
3111
2819
  try {
3112
- if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3113
- let HandlerCtor = getHandler(key);
3114
- if (!HandlerCtor) {
3115
- this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3116
- HandlerCtor = fallback;
3117
- }
3118
- this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
3119
- const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
3120
- const handler = new HandlerCtor(interaction, this.core, dispatch);
2820
+ if (!interaction.isAutocomplete()) await this.runMiddlewares(interaction);
2821
+ const HandlerCtor = matched ?? fallback;
2822
+ const dispatch = new DispatchContext(routeIdOf(HandlerCtor) ?? routeId);
2823
+ routeId = dispatch.routeId ?? routeId;
2824
+ const handler = this.buildHandler(HandlerCtor, interaction, dispatch, key, !matched);
3121
2825
  if (handler instanceof RepliableHandler) sender = handler.getSender();
3122
- if (!interaction.isAutocomplete()) {
3123
- const monitor = slowGateMonitor();
3124
- try {
3125
- await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3126
- } finally {
3127
- monitor?.report(dispatch.routeId);
3128
- }
2826
+ const refusal = interaction.isAutocomplete() ? null : await this.gateRefusal(HandlerCtor, interaction, dispatch.routeId);
2827
+ if (refusal) {
2828
+ await this.answer(refusal.caught, interaction, routeId, sender, report);
2829
+ return;
3129
2830
  }
3130
2831
  await handler.execute();
2832
+ report("handled");
2833
+ } catch (caught) {
2834
+ await this.answer(caught, interaction, routeId, sender, report);
2835
+ }
2836
+ }
2837
+ async answer(caught, interaction, routeId, sender, report) {
2838
+ try {
2839
+ await handleInteractionFault(caught, interaction, this.core, routeId, sender);
2840
+ } finally {
2841
+ report(outcomeFor(caught));
2842
+ }
2843
+ }
2844
+ buildHandler(HandlerCtor, interaction, dispatch, key, isFallback) {
2845
+ if (isFallback) this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${HandlerCtor.name}.`);
2846
+ this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
2847
+ return new HandlerCtor(interaction, this.core, dispatch);
2848
+ }
2849
+ async runMiddlewares(interaction) {
2850
+ for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
2851
+ }
2852
+ async gateRefusal(HandlerCtor, interaction, routeId) {
2853
+ const monitor = slowGateMonitor();
2854
+ try {
2855
+ await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), routeId ?? void 0, monitor?.observe);
2856
+ return null;
3131
2857
  } catch (caught) {
3132
- await handleInteractionFault(caught, interaction, this.core, sender);
2858
+ return { caught };
2859
+ } finally {
2860
+ monitor?.report(routeId);
3133
2861
  }
3134
2862
  }
3135
2863
  async handleInteraction(interaction) {
@@ -3174,47 +2902,41 @@ var InteractionDispatcher = class {
3174
2902
  }
3175
2903
  async handleSlashCommand(interaction) {
3176
2904
  const route = slashRouteOf(interaction);
3177
- await this.processInteraction(interaction, () => route, (key) => this.slashMap.get(key));
2905
+ await this.processInteraction(interaction, InteractionRoutes.Slash, () => route, (key) => this.slashMap.get(key));
3178
2906
  }
3179
2907
  async handleButton(interaction) {
3180
- await this.handleCustomIdInteraction(interaction, () => this.buttonMap, "Button");
2908
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.Button, () => this.buttonMap);
3181
2909
  }
3182
2910
  async handleModal(interaction) {
3183
- await this.handleCustomIdInteraction(interaction, () => this.modalMap, "Modal");
2911
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.Modal, () => this.modalMap);
3184
2912
  }
3185
2913
  async handleStringSelectMenu(interaction) {
3186
- await this.handleCustomIdInteraction(interaction, () => this.stringSelectMap, "String select menu");
2914
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.StringMenu, () => this.stringSelectMap);
3187
2915
  }
3188
2916
  async handleUserSelectMenu(interaction) {
3189
- await this.handleCustomIdInteraction(interaction, () => this.userSelectMap, "User select menu");
2917
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.UserMenu, () => this.userSelectMap);
3190
2918
  }
3191
2919
  async handleRoleSelectMenu(interaction) {
3192
- await this.handleCustomIdInteraction(interaction, () => this.roleSelectMap, "Role select menu");
2920
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.RoleMenu, () => this.roleSelectMap);
3193
2921
  }
3194
2922
  async handleChannelSelectMenu(interaction) {
3195
- await this.handleCustomIdInteraction(interaction, () => this.channelSelectMap, "Channel select menu");
2923
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.ChannelMenu, () => this.channelSelectMap);
3196
2924
  }
3197
2925
  async handleMentionableSelectMenu(interaction) {
3198
- await this.handleCustomIdInteraction(interaction, () => this.mentionableSelectMap, "Mentionable select menu");
2926
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.MentionableMenu, () => this.mentionableSelectMap);
3199
2927
  }
3200
2928
  async handleMessageContextMenu(interaction) {
3201
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
2929
+ await this.processInteraction(interaction, InteractionRoutes.MessageContextMenu, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
3202
2930
  }
3203
2931
  async handleUserContextMenu(interaction) {
3204
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
2932
+ await this.processInteraction(interaction, InteractionRoutes.UserContextMenu, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
3205
2933
  }
3206
2934
  async handleAutocomplete(interaction) {
3207
2935
  const route = slashRouteOf(interaction);
3208
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
2936
+ await this.processInteraction(interaction, InteractionRoutes.Autocomplete, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3209
2937
  }
3210
2938
  };
3211
2939
 
3212
- //#endregion
3213
- //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
3214
- function __decorateMetadata(k, v) {
3215
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3216
- }
3217
-
3218
2940
  //#endregion
3219
2941
  //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorate.js
3220
2942
  function __decorate(decorators, target, key, desc) {
@@ -3226,23 +2948,25 @@ function __decorate(decorators, target, key, desc) {
3226
2948
 
3227
2949
  //#endregion
3228
2950
  //#region src/bot/Bot.ts
2951
+ const DISPATCH_DRAIN_TIMEOUT_MS = 5e3;
2952
+ const DRAIN_TASK_TIMEOUT_MS = 6e3;
2953
+ const UNBIND_TIMEOUT_MS = 2e3;
2954
+ const LOGOUT_TIMEOUT_MS = 2e3;
3229
2955
  /**
3230
- * Discord bot implementation that manages client and controllers
3231
- *
3232
- * Don't instantiate this class directly. Use `core.bot` instead.
2956
+ * The Discord client and its controllers. Access it through `core.bot`.
3233
2957
  */
3234
- var Bot = class extends Plugin {
2958
+ var Bot = class {
3235
2959
  /** @internal */
3236
- logger = new Logger("Bot");
2960
+ logger = new Logger("Bot", { channel: "bot" });
3237
2961
  isInitialized = false;
3238
2962
  _client;
3239
2963
  interactions;
3240
2964
  events;
3241
- commands;
3242
2965
  emojiInjector;
2966
+ commands;
3243
2967
  emojis = Emojis;
3244
2968
  mentions = CommandMentions;
3245
- /** @internal For use in dev mode */
2969
+ /** @internal */
3246
2970
  async onHmr(event) {
3247
2971
  if (this.interactions) await this.interactions.onHmr(event);
3248
2972
  if (this.events) await this.events.onHmr(event);
@@ -3250,7 +2974,6 @@ var Bot = class extends Plugin {
3250
2974
  }
3251
2975
  /** @internal */
3252
2976
  constructor(core) {
3253
- super(core);
3254
2977
  this._client = new Client(core.config.bot.clientOptions);
3255
2978
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
3256
2979
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
@@ -3261,12 +2984,24 @@ var Bot = class extends Plugin {
3261
2984
  });
3262
2985
  }
3263
2986
  this.emojiInjector = new EmojiInjector(core);
3264
- core.shutdown.addTask(ShutdownPhase$1.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
2987
+ this.registerShutdownTasks(core);
3265
2988
  }
3266
- /**
3267
- * Initializes Discord client and all controllers
3268
- * @internal
3269
- */
2989
+ registerShutdownTasks(core) {
2990
+ core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
2991
+ this.stopAccepting();
2992
+ return Promise.resolve();
2993
+ }, UNBIND_TIMEOUT_MS);
2994
+ core.shutdown.addTask(ShutdownPhase.Drain, "drain-dispatch", () => this.drain(), DRAIN_TASK_TIMEOUT_MS);
2995
+ core.shutdown.addTask(ShutdownPhase.Logout, "stop-bot", async () => await this.stop(), LOGOUT_TIMEOUT_MS);
2996
+ }
2997
+ stopAccepting() {
2998
+ this.interactions?.stopAccepting();
2999
+ this.events?.stopAccepting();
3000
+ }
3001
+ async drain() {
3002
+ await Promise.allSettled([this.interactions?.drain(DISPATCH_DRAIN_TIMEOUT_MS), this.events?.drain(DISPATCH_DRAIN_TIMEOUT_MS)]);
3003
+ }
3004
+ /** @internal */
3270
3005
  async init() {
3271
3006
  if (this.isInitialized) return;
3272
3007
  this.isInitialized = true;
@@ -3282,27 +3017,19 @@ var Bot = class extends Plugin {
3282
3017
  this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
3283
3018
  }
3284
3019
  }
3285
- /**
3286
- * Stops the bot and cleans up connections
3287
- * @internal
3288
- */
3020
+ /** @internal */
3289
3021
  async stop() {
3290
3022
  this._client.removeAllListeners();
3291
3023
  await this.logout();
3292
3024
  }
3293
- /**
3294
- * Logs the bot into Discord using the configured token
3295
- */
3296
3025
  async login(token) {
3297
- this._client.once(Events.ClientReady, () => this.emit("ready"));
3026
+ const ready = Promise.withResolvers();
3027
+ this._client.once(Events.ClientReady, () => ready.resolve());
3298
3028
  this._client.login(token);
3299
- await this.waitFor("ready");
3029
+ await ready.promise;
3300
3030
  this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
3301
3031
  return this;
3302
3032
  }
3303
- /**
3304
- * Logs out and destroys the Discord client connection
3305
- */
3306
3033
  async logout() {
3307
3034
  await this._client.destroy();
3308
3035
  this.logger.info(chalk.bold.red("Logged out of Discord!"));
@@ -3310,248 +3037,19 @@ var Bot = class extends Plugin {
3310
3037
  get client() {
3311
3038
  return this._client;
3312
3039
  }
3313
- emit(event, ...args) {
3314
- return super.emit(event, ...args);
3315
- }
3316
- emitSafe(event, ...args) {
3317
- return super.emitSafe(event, ...args);
3318
- }
3319
- onListenerError(error, event) {
3320
- this.logger.error(`listener for ${String(event)} threw`, error);
3321
- }
3322
- };
3323
- __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3324
-
3325
- //#endregion
3326
- //#region src/subscribers/default/HandledException.ts
3327
- let HandledException = class HandledException extends WebhookLog {
3328
- report() {
3329
- return {
3330
- username: "Handled Exception",
3331
- components: [new HandledExceptionContainer(this.data).component],
3332
- files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3333
- };
3334
- }
3335
- };
3336
- HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3337
- var HandledExceptionContainer = class extends BuilderComponent {
3338
- constructor(data) {
3339
- super("container");
3340
- const { denial, uuid, source } = data;
3341
- 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"));
3342
- }
3343
- };
3344
- function faultSummary(denial, source) {
3345
- const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3346
- 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"}\``;
3347
- 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}\``;
3348
- }
3349
- function causeStack(denial) {
3350
- const { cause } = denial;
3351
- if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
3352
- if (typeof cause === "string") return stripAnsi(cause);
3353
- if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3354
- if (cause !== void 0) try {
3355
- return JSON.stringify(cause);
3356
- } catch {
3357
- return "[unserializable cause]";
3358
- }
3359
- return stripAnsi(denial.stack ?? "No cause recorded.");
3360
- }
3361
-
3362
- //#endregion
3363
- //#region src/subscribers/default/UnknownException.ts
3364
- let UnknownException = class UnknownException extends WebhookLog {
3365
- report() {
3366
- const { metadata } = this.data;
3367
- return {
3368
- username: "Unknown Exception",
3369
- components: [new UnhandledErrorContainer(this.data).component],
3370
- files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3371
- };
3372
- }
3373
- };
3374
- UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3375
- var UnhandledErrorContainer = class extends BuilderComponent {
3376
- constructor(data) {
3377
- super("container");
3378
- const { uuid, error, guild, user, metadata } = data;
3379
- 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)}\`\`\``));
3380
- this.addTimestampsIfAvailable(error);
3381
- this.addMetadataIfAvailable(metadata);
3382
- }
3383
- addTimestampsIfAvailable(error) {
3384
- if (!(error instanceof DiscordAPIError$1)) return;
3385
- const now = Date.now();
3386
- const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3387
- if (!snowflake) return void 0;
3388
- const interactionTs = timestampFromSnowflake(snowflake);
3389
- const diff = now - interactionTs;
3390
- const seconds = Math.floor(diff / 1e3);
3391
- const millis = diff % 1e3;
3392
- 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`));
3393
- }
3394
- addMetadataIfAvailable(metadata) {
3395
- if (!metadata) return;
3396
- this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Metadata")).addFileComponents((file) => file.setURL("attachment://metadata.json"));
3397
- }
3398
- };
3399
-
3400
- //#endregion
3401
- //#region src/subscribers/Bus.ts
3402
- /**
3403
- * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3404
- * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3405
- */
3406
- var Bus = class extends Plugin {
3407
- core;
3408
- logger = new Logger("Subscribers");
3409
- /** @internal */
3410
- name = "Subscribers";
3411
- isInitialized = false;
3412
- subscribersMap = /* @__PURE__ */ new Map();
3413
- executedOnceHandlers = /* @__PURE__ */ new Set();
3414
- webhookProbes = /* @__PURE__ */ new Map();
3415
- hmrHandler;
3416
- constructor(core) {
3417
- super(core);
3418
- this.core = core;
3419
- if (this.core.config.subscribers.path) {
3420
- if (!Envapter.isDevelopment && !Envapter.isTest) return;
3421
- this.hmrHandler = new HmrModuleHandler({
3422
- handlersDir: this.core.config.subscribers.path,
3423
- isHandler: this.isSubscriber.bind(this),
3424
- registerHandler: this.registerSubscriber.bind(this),
3425
- unregisterHandler: this.unregisterSubscriber.bind(this),
3426
- getArtifacts: this.getArtifacts.bind(this),
3427
- logger: this.logger
3428
- });
3429
- }
3430
- }
3431
- getArtifacts(ctor) {
3432
- return [Reflect.getMetadata(SubscribeMetadataKey, ctor).subscriber];
3433
- }
3434
- /** @internal */
3435
- async init() {
3436
- if (this.isInitialized) return;
3437
- this.isInitialized = true;
3438
- this.registerSubscriber(UnknownException);
3439
- this.registerSubscriber(HandledException);
3440
- const subscribersDir = this.core.config.subscribers.path;
3441
- if (subscribersDir) {
3442
- this.logger.info(chalk.bold(subscribersDir));
3443
- await this.loadSubscribers(subscribersDir);
3444
- const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3445
- this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
3446
- }
3447
- if (!Envapter.isTest) await this.verifyWebhooks();
3448
- }
3449
- /** @internal Exposed for tests. */
3450
- async verifyWebhooks() {
3451
- const missing = [];
3452
- await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3453
- const result = await WebhookLog.senderFor(url).verify();
3454
- if (result === "missing") missing.push(...envKeys);
3455
- else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
3456
- }));
3457
- if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3458
- }
3459
- /** @internal */
3460
- async onHmr(event) {
3461
- if (this.hmrHandler) await this.hmrHandler.handle(event);
3462
- }
3463
- async loadSubscribers(dir) {
3464
- await traverseDirectory(dir, (fullPath, relativePath, imported) => {
3465
- for (const exportName of Object.keys(imported)) {
3466
- const val = imported[exportName];
3467
- if (this.isSubscriber(val)) {
3468
- this.registerSubscriber(val);
3469
- this.hmrHandler?.trackHandler(fullPath, val);
3470
- this.logger.utils.registration(val.name, relativePath);
3471
- }
3472
- }
3473
- }, this.logger);
3474
- }
3475
- registerSubscriber(handler) {
3476
- if (handler.prototype instanceof WebhookLog) {
3477
- const envKey = WebhookLog.envKeyOf(handler);
3478
- const url = WebhookLog.urlOf(envKey);
3479
- if (url === null) {
3480
- this.logger.warn(`${chalk.bold(handler.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
3481
- return;
3482
- }
3483
- const envKeys = this.webhookProbes.get(url) ?? [];
3484
- if (!envKeys.includes(envKey)) envKeys.push(envKey);
3485
- this.webhookProbes.set(url, envKeys);
3486
- }
3487
- const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
3488
- let handlers = this.subscribersMap.get(options.subscriber);
3489
- if (!handlers) {
3490
- handlers = [];
3491
- this.subscribersMap.set(options.subscriber, handlers);
3492
- }
3493
- handlers.push({
3494
- ctor: handler,
3495
- frequency: options.frequency ?? "on"
3496
- });
3497
- }
3498
- unregisterSubscriber(handler, artifacts) {
3499
- const subscribers = artifacts ?? [...this.subscribersMap.keys()];
3500
- for (const subscriber of subscribers) {
3501
- const handlers = this.subscribersMap.get(subscriber);
3502
- if (!handlers) continue;
3503
- const index = handlers.findIndex((entry) => entry.ctor === handler);
3504
- if (index !== -1) handlers.splice(index, 1);
3505
- }
3506
- }
3507
- isSubscriber(obj) {
3508
- if (typeof obj !== "function") return false;
3509
- return obj.prototype instanceof Subscriber && Reflect.hasMetadata(SubscribeMetadataKey, obj);
3510
- }
3511
- /**
3512
- * Publishes an event to its subscribers and native listeners.
3513
- *
3514
- * Fire-and-forget, so subscriber handlers run asynchronously and this returns before they
3515
- * complete, and callers must not assume side effects have landed. Errors thrown by a subscriber
3516
- * are caught and logged, never surfaced here. A `'once'` subscriber is marked as executed when
3517
- * it starts (even if it throws), so it never runs twice.
3518
- *
3519
- * @param event - The subscription key to publish
3520
- * @param data - Payload passed to each subscriber
3521
- * @returns Whether the underlying emitter had native listeners for the event
3522
- */
3523
- publish(event, data) {
3524
- this.processSubscriber(event, data);
3525
- return super.emit(event, ...[data]);
3526
- }
3527
- async processSubscriber(subscriberName, data) {
3528
- const handlers = this.subscribersMap.get(subscriberName);
3529
- if (!handlers) return;
3530
- for (const entry of handlers) {
3531
- if (entry.frequency === "once" && this.executedOnceHandlers.has(entry.ctor)) continue;
3532
- try {
3533
- if (entry.frequency === "once") this.executedOnceHandlers.add(entry.ctor);
3534
- const Ctor = entry.ctor;
3535
- await new Ctor(data, this.core).execute();
3536
- } catch (err) {
3537
- this.logger.error(`Error in subscriber ${String(subscriberName)} handler ${entry.ctor.name}:`, err);
3538
- }
3539
- }
3540
- }
3541
3040
  };
3041
+ __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) })], Bot.prototype, "botToken", void 0);
3542
3042
 
3543
3043
  //#endregion
3544
3044
  //#region src/version.ts
3545
3045
  /** Package version */
3546
- const version = "0.1.0-next.3";
3046
+ const version = "0.1.0-next.5";
3547
3047
 
3548
3048
  //#endregion
3549
3049
  //#region src/Seedcord.ts
3550
3050
  /**
3551
- * Main Seedcord bot framework class
3552
- *
3553
- * Primary entry point for creating Discord bots with Seedcord.
3554
- * Manages component lifecycle and provides plugin support.
3051
+ * The gateway bot host. Opens a discord.js gateway session, discovers handlers, and runs
3052
+ * coordinated startup and shutdown. Attach plugins with `attach()`.
3555
3053
  */
3556
3054
  var Seedcord = class Seedcord extends Pluggable {
3557
3055
  config;
@@ -3561,83 +3059,86 @@ var Seedcord = class Seedcord extends Pluggable {
3561
3059
  augmentTarget = "@seedcord/gateway";
3562
3060
  /** @internal */
3563
3061
  version = version;
3564
- static isInstantiated = false;
3565
3062
  healthCheck;
3566
3063
  hmrManager;
3567
- /** @see {@link CoordinatedShutdown} */
3568
- shutdown;
3569
- /** @see {@link CoordinatedStartup} */
3570
- startup;
3571
3064
  /** @see {@link Bus} */
3572
3065
  bus;
3066
+ subscribers;
3573
3067
  /** @see {@link Bot} */
3574
3068
  bot;
3575
3069
  /** @see {@link IRateLimiter} */
3576
3070
  rateLimiter;
3577
3071
  /**
3578
- * Creates a new Seedcord instance
3579
- *
3580
3072
  * @param config - Bot configuration including paths and Discord client options
3581
3073
  * @throws A **SeedcordError** When attempting to create multiple instances (singleton)
3582
3074
  */
3583
3075
  constructor(config) {
3584
- if (Seedcord.isInstantiated) throw new SeedcordError(SeedcordErrorCode.CoreSingletonViolation);
3585
- Seedcord.isInstantiated = true;
3586
- const shutdown = new CoordinatedShutdown(config.shutdownEnabled);
3587
- const startup = new CoordinatedStartup();
3588
- super(shutdown, startup);
3076
+ super(new CoordinatedShutdown(), new CoordinatedStartup());
3589
3077
  this.config = config;
3590
3078
  installNodeDefaults(config.logger);
3591
3079
  setBotColor(config.botColor);
3592
- this.shutdown = shutdown;
3593
- this.startup = startup;
3594
3080
  this.hmrManager = new HmrManager();
3595
3081
  this.hmrManager.init();
3596
3082
  this.bus = new Bus(this);
3083
+ this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
3597
3084
  this.bot = new Bot(this);
3598
3085
  this.rateLimiter = config.store ?? new MemoryRateLimiter();
3599
- this.healthCheck = new HealthCheck(this.shutdown, config.healthCheck);
3086
+ this.healthCheck = HealthCheck.fromOption(this.shutdown, config.healthCheck);
3600
3087
  this.registerStartupTasks();
3601
3088
  }
3602
3089
  /** The bot's discord username, populated after login. */
3603
3090
  get username() {
3604
3091
  return this.bot.client.user?.username;
3605
3092
  }
3093
+ pluginCapabilities() {
3094
+ return {
3095
+ client: this.bot.client,
3096
+ token: this.bot.botToken,
3097
+ rest: this.bot.client.rest
3098
+ };
3099
+ }
3606
3100
  static reset() {
3607
- Seedcord.isInstantiated = false;
3101
+ super.reset();
3608
3102
  LoggerChannelRegistry.instance.reset();
3609
3103
  }
3610
3104
  registerStartupTasks() {
3611
3105
  if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
3612
- this.startup.addTask(StartupPhase$1.Configuration, "Bus Initialization", async () => {
3106
+ this.startup.addTask(StartupPhase.Configuration, "bus-initialization", async () => {
3613
3107
  this.bus.logger.utils.initialization("Subscribers", "start");
3614
- await this.bus.init();
3108
+ await this.subscribers.init();
3615
3109
  this.bus.logger.utils.initialization("Subscribers", "end");
3616
3110
  });
3617
- this.startup.addTask(StartupPhase$1.Instantiation, "Bot Initialization", async () => {
3111
+ this.startup.addTask(StartupPhase.Login, "bot-initialization", async () => {
3618
3112
  this.bot.logger.utils.initialization("Bot", "start");
3619
3113
  await this.bot.init();
3620
3114
  this.bot.logger.utils.initialization("Bot", "end");
3621
3115
  });
3622
- this.startup.addTask(StartupPhase$1.Ready, "Health Check", async () => {
3623
- this.healthCheck.logger.utils.initialization("HealthCheck", "start");
3624
- await this.healthCheck.init();
3625
- this.healthCheck.logger.utils.initialization("HealthCheck", "end");
3116
+ const { healthCheck } = this;
3117
+ if (healthCheck) this.startup.addTask(StartupPhase.Ready, "health-check", async () => {
3118
+ healthCheck.logger.utils.initialization("HealthCheck", "start");
3119
+ await healthCheck.init();
3120
+ healthCheck.logger.utils.initialization("HealthCheck", "end");
3626
3121
  });
3627
3122
  }
3628
3123
  /**
3629
3124
  * Starts the bot and runs all initialization tasks
3630
3125
  *
3631
- * @returns This Seedcord instance when fully initialized
3126
+ * @returns This Seedcord instance when initialized
3632
3127
  */
3633
3128
  async start() {
3634
- await super.init();
3129
+ try {
3130
+ await super.init();
3131
+ } catch (caught) {
3132
+ await this.shutdown.run(1, false);
3133
+ Seedcord.reset();
3134
+ throw caught;
3135
+ }
3635
3136
  return this;
3636
3137
  }
3637
3138
  registerHmrAwareModules() {
3638
- this.startup.addTask(StartupPhase$1.Configuration, "HMR Registration", async () => {
3139
+ this.startup.addTask(StartupPhase.Configuration, "hmr-registration", async () => {
3639
3140
  this.hmrManager.register(this.bot);
3640
- this.hmrManager.register(this.bus);
3141
+ this.hmrManager.register(this.subscribers);
3641
3142
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
3642
3143
  await Promise.resolve();
3643
3144
  });
@@ -3645,5 +3146,5 @@ var Seedcord = class Seedcord extends Pluggable {
3645
3146
  };
3646
3147
 
3647
3148
  //#endregion
3648
- export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Pluggable, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, ShutdownPhase, SlashHandler, StartupPhase, Subscribe, Subscriber, WebhookLog, WebhookUrl, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3149
+ export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscriber, WebhookLog, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3649
3150
  //# sourceMappingURL=index.mjs.map