@seedcord/gateway 0.8.0 → 0.9.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,17 +1,16 @@
1
1
  import "reflect-metadata";
2
2
  import { DRAIN_TASK_TIMEOUT_MS, DRAIN_WINDOW_MS, HealthCheck, InFlight, ShutdownPhase, StartupPhase, setEnvaptBaseDir, shutdownOf } from "@seedcord/core/node/internal";
3
3
  import { WinstonConsoleSink, WinstonFileSink, installNodeConsole } from "@seedcord/logger/node";
4
- import { COMMAND_DEPLOY_TIMEOUT_MS, CommandRegistry, EventMetadataKey, EventMiddlewareMetadataKey, FixedScope, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, MiddlewareRegistry, MissingPermissions, NoticeCard, PermissionNames, PublishDefault, SubscriberLoader, accessorStore, attachmentsOf, bindBotColor, busLoggerOf, clearStore, connectDevChannel, decodeComponentRoute, deferFlags, eventResultFor, guardedAccessor, interactionMiddleware, interactionRoutesOf, isEmojiTuple, logUnknownFault, outcomeFor, pickNotice, queuedMsFor, readBotToken, reportDispatch, reportedWrite, resultFor, runAfter, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
4
+ import { COMMAND_DEPLOY_TIMEOUT_MS, CommandRegistry, EventMetadataKey, EventMiddlewareMetadataKey, FixedScope, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, MiddlewareRegistry, MissingPermissions, NoticeCard, OnceTracker, PermissionNames, PublishDefault, SubscriberLoader, accessorStore, attachmentsOf, bindBotColor, busLoggerOf, clearStore, connectDevChannel, decodeComponentRoute, deferFlags, eventResultFor, guardedAccessor, interactionMiddleware, interactionRoutesOf, isEmojiTuple, logUnknownFault, outcomeFor, pickNotice, queuedMsFor, readBotToken, reportDispatch, reportedWrite, resultFor, runAfter, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
5
5
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
6
6
  import { SeedcordError, SeedcordTypeError, throwSingleOrAggregate } from "@seedcord/errors/internal";
7
- import { ArraySourceBase, BaseHandler as BaseHandler$1, BaseReplySender, BuilderComponent, Bus, CursorSourceBase, CustomId, DispatchContext, Fault, InteractionKind, Notice, PaginatorBase, RepliableHandler as RepliableHandler$1, ResolvedEmoji, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, assertPermissions, defineGate } from "@seedcord/core";
7
+ import { ArraySourceBase, BaseHandler as BaseHandler$1, BaseReplySender, BuilderComponent, CursorSourceBase, CustomId, DispatchContext, Fault, InteractionKind, Notice, PaginatorBase, RepliableHandler as RepliableHandler$1, ResolvedEmoji, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, assertPermissions, defineGate } from "@seedcord/core";
8
8
  import { Logger } from "@seedcord/logger";
9
9
  import { ButtonStyle, Client, ComponentType, DiscordAPIError, Events, GatewayIntentBits, Guild, GuildMember, IntentsBitField, InteractionContextType, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, User } from "discord.js";
10
10
  import { ButtonBuilder, TextDisplayBuilder } from "@discordjs/builders";
11
11
  import { asError, buildSlashRoute, isStringArray } from "@seedcord/utils/internal";
12
12
  import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
13
13
  import { CoordinatedShutdown, CoordinatedStartup, ServerHost } from "@seedcord/core/node";
14
- import { MemoryRateLimiter } from "@seedcord/rate-limiter";
15
14
  import { HostAugmentTarget, HostVersion, SeedcordBrand } from "@seedcord/types/internal";
16
15
  import { Envapter } from "envapt";
17
16
  import { HmrModuleHandler, HmrModuleHandler as HmrModuleHandler$1 } from "@seedcord/core/hmr";
@@ -25,8 +24,6 @@ export * from "@seedcord/core"
25
24
 
26
25
  export * from "@seedcord/logger"
27
26
 
28
- export * from "@seedcord/rate-limiter"
29
-
30
27
  export * from "@seedcord/utils"
31
28
 
32
29
  export * from "@seedcord/event-emitter"
@@ -97,7 +94,7 @@ function RegisterEvent(...defs) {
97
94
  * Attaches gates to a handler. The gates run before `execute`, and a gate refusing stops the handler with
98
95
  * the reply or drop the gate threw. Multiple gates are ANDed, so each must pass. A gate that requires a
99
96
  * context the handler does not provide (a button gate on a slash handler, an interaction gate on an event
100
- * handler) is a compile error at this line, and the error names the gate and the handler kind. Place it
97
+ * handler) is a compile error at this line, and the error message shows the gate and the handler kind. Place it
101
98
  * above the route decorator.
102
99
  *
103
100
  * Combine gates with {@link and} or {@link or} to build a single gate from multiple arms.
@@ -738,6 +735,10 @@ function slashRouteOf(interaction) {
738
735
  * ```
739
736
  */
740
737
  var SlashHandler = class extends InteractionHandler {
738
+ /** The selected command's path, such as `search/docs`. */
739
+ get route() {
740
+ return slashRouteOf(this.event);
741
+ }
741
742
  /**
742
743
  * The typed options for this command's route. Only the getters for kinds this command declares appear.
743
744
  * Each getter's name argument is restricted to that command's options of its kind.
@@ -755,7 +756,7 @@ var SlashHandler = class extends InteractionHandler {
755
756
  return this.event.options;
756
757
  }
757
758
  /**
758
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
759
+ * Run the arm for the selected command. Use this only when the handler is registered for several
759
760
  * routes.
760
761
  *
761
762
  * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
@@ -779,7 +780,7 @@ var SlashHandler = class extends InteractionHandler {
779
780
  * ```
780
781
  */
781
782
  async match(arms) {
782
- const route = slashRouteOf(this.event);
783
+ const route = this.route;
783
784
  const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
784
785
  if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
785
786
  this.constructor.name,
@@ -828,12 +829,12 @@ var UserContextMenuHandler = class extends InteractionHandler {
828
829
  get targetMember() {
829
830
  return this.event.targetMember;
830
831
  }
831
- /** Which of the registered commands fired. */
832
- get commandName() {
832
+ /** The selected context-menu command's name, without the interaction-kind prefix. */
833
+ get route() {
833
834
  return this.event.commandName;
834
835
  }
835
836
  /**
836
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
837
+ * Run the arm for the selected command. Use this only when the handler is registered for several
837
838
  * names.
838
839
  *
839
840
  * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
@@ -858,7 +859,7 @@ var UserContextMenuHandler = class extends InteractionHandler {
858
859
  * ```
859
860
  */
860
861
  async match(arms) {
861
- return await armFor(this, arms, this.event.commandName)(this.target, this.targetMember);
862
+ return await armFor(this, arms, this.route)(this.target, this.targetMember);
862
863
  }
863
864
  };
864
865
  /**
@@ -884,12 +885,12 @@ var MessageContextMenuHandler = class extends InteractionHandler {
884
885
  get target() {
885
886
  return this.event.targetMessage;
886
887
  }
887
- /** Which of the registered commands fired. */
888
- get commandName() {
888
+ /** The selected context-menu command's name, without the interaction-kind prefix. */
889
+ get route() {
889
890
  return this.event.commandName;
890
891
  }
891
892
  /**
892
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
893
+ * Run the arm for the selected command. Use this only when the handler is registered for several
893
894
  * names.
894
895
  *
895
896
  * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
@@ -913,7 +914,7 @@ var MessageContextMenuHandler = class extends InteractionHandler {
913
914
  * ```
914
915
  */
915
916
  async match(arms) {
916
- return await armFor(this, arms, this.event.commandName)(this.target);
917
+ return await armFor(this, arms, this.route)(this.target);
917
918
  }
918
919
  };
919
920
 
@@ -936,7 +937,7 @@ var BaseHandler = class extends BaseHandler$1 {
936
937
  *
937
938
  * Pass the command route(s) from the generated registry as the generic, the same string(s) as
938
939
  * `@AutocompleteRoute`. Branch on the focused field with `this.match`, read already-entered sibling options
939
- * with `this.options`, and find which command fired with `this.route`.
940
+ * with `this.options`, and read the selected command's path from `this.route`.
940
941
  *
941
942
  * @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'search'` or `'search' | 'find'`.
942
943
  * @typeParam Cache - The interaction cache state. The command's contexts set it.
@@ -1016,7 +1017,7 @@ var AutocompleteHandler = class extends BaseHandler {
1016
1017
  interactionId: this.event.id
1017
1018
  }, "respond", () => this.event.respond(choices));
1018
1019
  }
1019
- /** The firing command route, for a field whose completion differs per registered command. */
1020
+ /** The selected command's path, such as `search/docs`, without the interaction-kind prefix. */
1020
1021
  get route() {
1021
1022
  return slashRouteOf(this.event);
1022
1023
  }
@@ -1051,11 +1052,20 @@ var ComponentHandler = class extends InteractionHandler {
1051
1052
  return this.sender.deferUpdate();
1052
1053
  }
1053
1054
  decoded;
1054
- get route() {
1055
+ get decodedRoute() {
1055
1056
  this.decoded ??= decodeComponentRoute(this.constructor, this.event.customId);
1056
1057
  return this.decoded;
1057
1058
  }
1058
1059
  /**
1060
+ * The matched custom-ID prefix, such as `approve`, without encoded params.
1061
+ *
1062
+ * @throws A **StaleCustomId** when the wire was encoded under an older shape.
1063
+ * @throws An **InvalidCustomId** when the wire is corrupt.
1064
+ */
1065
+ get route() {
1066
+ return this.decodedRoute.prefix;
1067
+ }
1068
+ /**
1059
1069
  * The decoded params of the single route this handler is registered for.
1060
1070
  *
1061
1071
  * Reading this decodes `this.event.customId` once and caches the result. On a handler registered
@@ -1065,7 +1075,7 @@ var ComponentHandler = class extends InteractionHandler {
1065
1075
  * @throws An **InvalidCustomId** when the wire is corrupt. The controller boundary turns it into a reply.
1066
1076
  */
1067
1077
  get params() {
1068
- return this.route.params;
1078
+ return this.decodedRoute.params;
1069
1079
  }
1070
1080
  /**
1071
1081
  * Run the arm for whichever route the component was minted from. Use this only when the handler is
@@ -1096,7 +1106,7 @@ var ComponentHandler = class extends InteractionHandler {
1096
1106
  * ```
1097
1107
  */
1098
1108
  async match(arms) {
1099
- const { prefix, params } = this.route;
1109
+ const { prefix, params } = this.decodedRoute;
1100
1110
  const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1101
1111
  if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
1102
1112
  this.constructor.name,
@@ -1175,7 +1185,7 @@ var ModalHandler = class extends ComponentHandler {
1175
1185
  }
1176
1186
  ensureSourceMessage(method) {
1177
1187
  if (this.event.isFromMessage()) return;
1178
- throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
1188
+ throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.dispatch.routeId]);
1179
1189
  }
1180
1190
  };
1181
1191
 
@@ -1364,7 +1374,7 @@ var EventHandler = class extends BaseHandler {
1364
1374
  this.firedEvent = eventName;
1365
1375
  }
1366
1376
  /**
1367
- * Run the arm for whichever event fired. Use this only when the handler is registered for several
1377
+ * Run the arm for the current event. Use this only when the handler is registered for several
1368
1378
  * events. A single-event handler reads `this.event` directly. On a multi-event handler `this.event` is
1369
1379
  * `never`, so match is the only way to read the payload.
1370
1380
  *
@@ -1421,7 +1431,7 @@ var EventMiddleware = class extends BaseHandler {
1421
1431
  this.firedEvent = eventName;
1422
1432
  }
1423
1433
  /**
1424
- * The event that fired this middleware. Read it on a catchall or multi-event middleware, where `this.event`
1434
+ * The name of the event being processed. Read it on a catchall or multi-event middleware, where `this.event`
1425
1435
  * is `never`, to do work that does not depend on the payload shape. A single-event middleware reads
1426
1436
  * `this.event` directly and does not need this.
1427
1437
  */
@@ -1818,19 +1828,20 @@ const eventMiddleware = (ctor) => {
1818
1828
  */
1819
1829
  var EventDispatcher = class {
1820
1830
  core;
1831
+ inFlight;
1821
1832
  logger = new Logger("Events", { channel: "events" });
1822
1833
  isInitialized = false;
1823
1834
  eventMap = /* @__PURE__ */ new Map();
1824
1835
  middlewares = new MiddlewareRegistry(eventMiddleware);
1825
- executedOnceHandlers = /* @__PURE__ */ new Set();
1836
+ onceRuns = new OnceTracker();
1826
1837
  attachedEvents = /* @__PURE__ */ new Set();
1827
- inFlight = new InFlight(this.logger, "Events");
1828
1838
  loading = false;
1829
1839
  loadedHandlers = [];
1830
1840
  loadedMiddlewares = [];
1831
1841
  hmrHandler;
1832
- constructor(core) {
1842
+ constructor(core, inFlight) {
1833
1843
  this.core = core;
1844
+ this.inFlight = inFlight;
1834
1845
  const eventsDir = this.core.config.bot.events.path;
1835
1846
  if (!eventsDir) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["EventDispatcher", "events"]);
1836
1847
  if (!Envapter.isDevelopment && !Envapter.isTest) return;
@@ -1980,26 +1991,19 @@ var EventDispatcher = class {
1980
1991
  const handlerEntries = this.eventMap.get(eventName);
1981
1992
  this.logger.debug(`Attaching ${paint.sky.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
1982
1993
  this.core.bot.client.on(eventName, (...args) => {
1983
- if (this.inFlight.closed) return;
1984
- const run = this.processEvent(eventName, args).catch((caught) => {
1985
- const error = asError(caught);
1986
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
1987
- this.core.bus[PublishDefault]("unhandledEventError", { error });
1988
- });
1989
- this.inFlight.track(run);
1994
+ this.inFlight.admit(() => this.processEvent(eventName, args).catch((caught) => this.reportUnhandled(caught)));
1990
1995
  });
1991
1996
  }
1992
- stopAccepting() {
1993
- this.inFlight.close();
1994
- }
1995
- drain(timeoutMs) {
1996
- return this.inFlight.drain(timeoutMs);
1997
+ reportUnhandled(caught) {
1998
+ const error = asError(caught);
1999
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2000
+ this.core.bus[PublishDefault]("unhandledEventError", { error });
1997
2001
  }
1998
2002
  async processEvent(eventName, args) {
1999
2003
  const startedAt = performance.now();
2000
2004
  const handlerEntries = this.eventMap.get(eventName);
2001
2005
  if (!handlerEntries || handlerEntries.length === 0) return;
2002
- const handlersToExecute = handlerEntries.filter((entry) => entry.frequency !== "once" || !this.executedOnceHandlers.has(entry.ctor));
2006
+ const handlersToExecute = handlerEntries.filter((entry) => !this.onceRuns.isSpent(entry, eventName));
2003
2007
  if (handlersToExecute.length === 0) return;
2004
2008
  const dispatch = new DispatchContext(`event:${String(eventName)}`);
2005
2009
  reportEventDispatching(this.core, dispatch.id, eventName, args);
@@ -2009,13 +2013,8 @@ var EventDispatcher = class {
2009
2013
  try {
2010
2014
  stopped = await this.runMiddlewares(eventName, args, dispatch, ran);
2011
2015
  if (stopped) return;
2012
- for (const entry of handlersToExecute) {
2013
- if (entry.frequency === "once") {
2014
- if (this.executedOnceHandlers.has(entry.ctor)) continue;
2015
- this.executedOnceHandlers.add(entry.ctor);
2016
- }
2017
- handlers.push(await this.processHandler(eventName, entry.ctor, args, dispatch));
2018
- }
2016
+ const results = handlersToExecute.filter((entry) => this.onceRuns.claim(entry, eventName)).map((entry) => this.processHandler(eventName, entry.ctor, args, dispatch));
2017
+ handlers.push(...await Promise.all(results));
2019
2018
  } finally {
2020
2019
  const result = eventResultFor(stopped, handlers);
2021
2020
  reportEventDispatched(this.core, dispatch.id, eventName, result, startedAt);
@@ -2024,7 +2023,7 @@ var EventDispatcher = class {
2024
2023
  }
2025
2024
  async processHandler(eventName, Ctor, args, dispatch) {
2026
2025
  try {
2027
- this.logger.debug(`Processing ${paint.sky.bold(eventName)} with ${paint.mute(Ctor.name)}`);
2026
+ this.logger.trace(`Processing ${paint.sky.bold(eventName)} with ${paint.mute(Ctor.name)}`);
2028
2027
  const handler = new Ctor(args, this.core, dispatch, eventName);
2029
2028
  const eventCtx = eventGateContext(eventName, args, this.core, dispatch);
2030
2029
  await runHandlerGates(Ctor, eventCtx);
@@ -2034,12 +2033,16 @@ var EventDispatcher = class {
2034
2033
  outcome: "handled"
2035
2034
  };
2036
2035
  } catch (caught) {
2037
- handleEventFault(caught, {
2038
- eventName: String(eventName),
2039
- handlerName: Ctor.name,
2040
- args,
2041
- dispatch
2042
- }, this.core);
2036
+ try {
2037
+ handleEventFault(caught, {
2038
+ eventName: String(eventName),
2039
+ handlerName: Ctor.name,
2040
+ args,
2041
+ dispatch
2042
+ }, this.core);
2043
+ } catch (reportFailed) {
2044
+ this.reportUnhandled(reportFailed);
2045
+ }
2043
2046
  return {
2044
2047
  handler: Ctor.name,
2045
2048
  ...resultFor(caught)
@@ -2112,7 +2115,8 @@ async function handleInteractionFault(caught, interaction, core, dispatch, sende
2112
2115
  user: interaction.user,
2113
2116
  metadata: interaction
2114
2117
  });
2115
- await sendGuarded(sender ?? new ReplySender(interaction, dispatch, core.bus), response, error instanceof Notice ? error.ephemeral : true);
2118
+ const liveSender = sender ?? new ReplySender(interaction, dispatch, core.bus);
2119
+ await sendGuarded(() => liveSender.send(response, { ephemeral: error instanceof Notice ? error.ephemeral : true }));
2116
2120
  }
2117
2121
  async function sendEmptyChoices(interaction, core, dispatch) {
2118
2122
  const telemetry = {
@@ -2120,21 +2124,17 @@ async function sendEmptyChoices(interaction, core, dispatch) {
2120
2124
  dispatch,
2121
2125
  interactionId: interaction.id
2122
2126
  };
2123
- try {
2124
- await reportedWrite(telemetry, "respond", () => interaction.respond([]));
2125
- } catch (error) {
2126
- logger.debug(`autocomplete empty-choices send failed: ${String(error)}`);
2127
- }
2127
+ await sendGuarded(() => reportedWrite(telemetry, "respond", () => interaction.respond([])));
2128
2128
  }
2129
- async function sendGuarded(sender, response, ephemeral) {
2129
+ async function sendGuarded(send) {
2130
2130
  try {
2131
- await sender.send(response, { ephemeral });
2131
+ await send();
2132
2132
  } catch (error) {
2133
2133
  if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
2134
2134
  logger.debug(`reply send hit harmless code ${error.code}`);
2135
2135
  return;
2136
2136
  }
2137
- logger.error("reply send failed", error);
2137
+ logger.error("fault response send failed", error);
2138
2138
  }
2139
2139
  }
2140
2140
 
@@ -2142,6 +2142,7 @@ async function sendGuarded(sender, response, ephemeral) {
2142
2142
  //#region src/bot/controllers/InteractionDispatcher.ts
2143
2143
  var InteractionDispatcher = class {
2144
2144
  core;
2145
+ inFlight;
2145
2146
  logger = new Logger("Interactions", { channel: "interactions" });
2146
2147
  isInitialized = false;
2147
2148
  maps = {
@@ -2160,13 +2161,13 @@ var InteractionDispatcher = class {
2160
2161
  handlerFiles = /* @__PURE__ */ new Map();
2161
2162
  keysToIgnore = /* @__PURE__ */ new Set();
2162
2163
  middlewares = new MiddlewareRegistry(interactionMiddleware);
2163
- inFlight = new InFlight(this.logger, "Interactions");
2164
2164
  loading = false;
2165
2165
  loadedHandlers = [];
2166
2166
  loadedMiddlewares = [];
2167
2167
  hmrHandler;
2168
- constructor(core) {
2168
+ constructor(core, inFlight) {
2169
2169
  this.core = core;
2170
+ this.inFlight = inFlight;
2170
2171
  const ignoredKeysFromConfig = hasKeys(this.core.config.bot.interactions, ["ignoreCustomIds"]) ? this.core.config.bot.interactions.ignoreCustomIds : void 0;
2171
2172
  if (ignoredKeysFromConfig) for (const ignoredKey of ignoredKeysFromConfig) this.keysToIgnore.add(ignoredKey);
2172
2173
  this.keysToIgnore.add(CONFIRM_DEF);
@@ -2304,22 +2305,16 @@ var InteractionDispatcher = class {
2304
2305
  }
2305
2306
  attachToClient() {
2306
2307
  this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
2307
- if (this.inFlight.closed) return;
2308
- this.core.bus[PublishDefault]("anyInteraction", { interaction });
2309
- const run = this.handleInteraction(interaction).catch((caught) => {
2310
- const error = asError(caught);
2311
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2312
- this.core.bus[PublishDefault]("unhandledInteractionError", { error });
2308
+ this.inFlight.admit(() => {
2309
+ this.core.bus[PublishDefault]("anyInteraction", { interaction });
2310
+ return this.handleInteraction(interaction).catch((caught) => {
2311
+ const error = asError(caught);
2312
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2313
+ this.core.bus[PublishDefault]("unhandledInteractionError", { error });
2314
+ });
2313
2315
  });
2314
- this.inFlight.track(run);
2315
2316
  });
2316
2317
  }
2317
- stopAccepting() {
2318
- this.inFlight.close();
2319
- }
2320
- drain(timeoutMs) {
2321
- return this.inFlight.drain(timeoutMs);
2322
- }
2323
2318
  async handleCustomIdInteraction(interaction, kind) {
2324
2319
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
2325
2320
  const prefix = prefixOf(interaction.customId);
@@ -2380,7 +2375,7 @@ var InteractionDispatcher = class {
2380
2375
  }
2381
2376
  buildHandler(HandlerCtor, interaction, dispatch, key, isFallback) {
2382
2377
  if (isFallback) this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${HandlerCtor.name}.`);
2383
- this.logger.debug(`Processing ${paint.sky.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
2378
+ this.logger.trace(`Processing ${paint.sky.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
2384
2379
  return new HandlerCtor(interaction, this.core, dispatch);
2385
2380
  }
2386
2381
  reporterFor(row) {
@@ -2535,6 +2530,7 @@ var Bot = class {
2535
2530
  #logger = new Logger("Bot", { channel: "bot" });
2536
2531
  /** @internal */
2537
2532
  [loggerSlot] = this.#logger;
2533
+ #inFlight = new InFlight(this.#logger, "Dispatch");
2538
2534
  #isInitialized = false;
2539
2535
  #readyToDeploy = Promise.withResolvers();
2540
2536
  #intents;
@@ -2553,8 +2549,8 @@ var Bot = class {
2553
2549
  constructor(core) {
2554
2550
  this.#intents = core.config.bot.clientOptions.intents;
2555
2551
  this.#client = new Client(core.config.bot.clientOptions);
2556
- if (core.config.bot.interactions.path) this.#interactions = new InteractionDispatcher(core);
2557
- if (core.config.bot.events.path) this.#events = new EventDispatcher(core);
2552
+ if (core.config.bot.interactions.path) this.#interactions = new InteractionDispatcher(core, this.#inFlight);
2553
+ if (core.config.bot.events.path) this.#events = new EventDispatcher(core, this.#inFlight);
2558
2554
  if (core.config.bot.commands.path) this.#commandRegistry = new CommandRegistry(core);
2559
2555
  this.#emojiInjector = new EmojiInjector(core);
2560
2556
  this.#registerStartupTasks(core);
@@ -2575,39 +2571,36 @@ var Bot = class {
2575
2571
  return application.id;
2576
2572
  }
2577
2573
  #registerStartupTasks(core) {
2574
+ const interactions = this.#interactions;
2575
+ if (interactions) core.startup.addTask(StartupPhase.Configuration, "interactions-initialization", () => interactions.init());
2576
+ const events = this.#events;
2577
+ if (events) core.startup.addTask(StartupPhase.Configuration, "events-initialization", () => events.init());
2578
2578
  core.startup.addTask(StartupPhase.Login, "bot-initialization", async (signal) => {
2579
2579
  this.#logger.utils.initialization("Bot", "start");
2580
2580
  await this.#init(signal);
2581
2581
  this.#logger.utils.initialization("Bot", "end");
2582
2582
  });
2583
2583
  const registry = this.#commandRegistry;
2584
- if (!registry) return;
2585
- core.startup.addTask(StartupPhase.Login, "command-deploy", (signal) => this.#deployCommands(registry, signal), COMMAND_DEPLOY_TIMEOUT_MS);
2584
+ if (registry) core.startup.addTask(StartupPhase.Login, "command-deploy", (signal) => this.#deployCommands(registry, signal), COMMAND_DEPLOY_TIMEOUT_MS);
2585
+ core.startup.addTask(StartupPhase.Ready, "start-dispatch", () => {
2586
+ this.#inFlight.open();
2587
+ return Promise.resolve();
2588
+ });
2586
2589
  }
2587
2590
  #registerShutdownTasks(core) {
2588
2591
  core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
2589
- this.#stopAccepting();
2592
+ this.#inFlight.close();
2590
2593
  return Promise.resolve();
2591
2594
  }, UNBIND_TIMEOUT_MS);
2592
- core.shutdown.addTask(ShutdownPhase.Drain, "drain-dispatch", () => this.#drain(), DRAIN_TASK_TIMEOUT_MS);
2595
+ core.shutdown.addTask(ShutdownPhase.Drain, "drain-dispatch", () => this.#inFlight.drain(DRAIN_WINDOW_MS), DRAIN_TASK_TIMEOUT_MS);
2593
2596
  core.shutdown.addTask(ShutdownPhase.Logout, "stop-bot", async () => await this.#stop(), LOGOUT_TIMEOUT_MS);
2594
2597
  }
2595
- #stopAccepting() {
2596
- this.#interactions?.stopAccepting();
2597
- this.#events?.stopAccepting();
2598
- }
2599
- async #drain() {
2600
- await Promise.allSettled([this.#interactions?.drain(DRAIN_WINDOW_MS), this.#events?.drain(DRAIN_WINDOW_MS)]);
2601
- }
2602
2598
  async #init(signal) {
2603
2599
  if (this.#isInitialized) return;
2604
2600
  this.#isInitialized = true;
2605
2601
  signal.addEventListener("abort", () => this.#readyToDeploy.resolve(false), { once: true });
2606
2602
  try {
2607
- const token = this.botToken;
2608
- if (this.#interactions) await this.#interactions.init();
2609
- if (this.#events) await this.#events.init();
2610
- await this.#login(token);
2603
+ await this.#login(this.botToken);
2611
2604
  await this.#emojiInjector.init();
2612
2605
  } catch (error) {
2613
2606
  this.#readyToDeploy.resolve(false);
@@ -2629,8 +2622,7 @@ var Bot = class {
2629
2622
  async #login(token) {
2630
2623
  const ready = Promise.withResolvers();
2631
2624
  this.#client.once(Events.ClientReady, () => ready.resolve());
2632
- this.#client.login(token);
2633
- await ready.promise;
2625
+ await Promise.all([this.#client.login(token), ready.promise]);
2634
2626
  this.#logger.info(`Logged in as ${paint.sky.bold(this.#client.user?.username)}!`);
2635
2627
  return this;
2636
2628
  }
@@ -2646,7 +2638,7 @@ var Bot = class {
2646
2638
  //#endregion
2647
2639
  //#region src/version.ts
2648
2640
  /** Package version */
2649
- const version = "0.8.0";
2641
+ const version = "0.9.0-next.0";
2650
2642
 
2651
2643
  //#endregion
2652
2644
  //#region src/Seedcord.ts
@@ -2664,8 +2656,6 @@ var Seedcord = class extends ServerHost {
2664
2656
  [HostVersion] = version;
2665
2657
  #healthCheck;
2666
2658
  #hmrManager;
2667
- /** @see {@link Bus} */
2668
- bus;
2669
2659
  #subscribers;
2670
2660
  /** @see {@link Bot} */
2671
2661
  bot;
@@ -2675,24 +2665,20 @@ var Seedcord = class extends ServerHost {
2675
2665
  get applicationId() {
2676
2666
  return this.bot.applicationId;
2677
2667
  }
2678
- /** @see {@link IRateLimiter} */
2679
- rateLimiter;
2680
2668
  /**
2681
2669
  * @param config - Bot configuration including paths and Discord client options
2682
2670
  * @throws A **SeedcordError** when another `Seedcord` exists in this process and has not shut down.
2683
2671
  * @throws A **SeedcordError** when the running Node or Bun is older than the range in `engines`.
2684
2672
  */
2685
2673
  constructor(config) {
2686
- super("gateway", new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup());
2674
+ super("gateway", new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup(), config.kv);
2687
2675
  this.config = config;
2688
2676
  bindBotColor(() => this.config.botColor);
2689
2677
  this.#hmrManager = new HmrManager();
2690
2678
  this.#hmrManager.init(connectDevChannel());
2691
- this.bus = new Bus(this);
2692
2679
  this.#subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
2693
2680
  this.bot = new Bot(this);
2694
2681
  this.rest = this.bot.client.rest;
2695
- this.rateLimiter = config.store ?? new MemoryRateLimiter();
2696
2682
  this.#healthCheck = HealthCheck.fromOption(shutdownOf(this), config.healthCheck);
2697
2683
  this.#registerStartupTasks();
2698
2684
  }