@seedcord/http 0.1.0-next.1 → 0.1.0-next.2

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.
@@ -2,14 +2,14 @@ import { n as __reExport, t as __exportAll } from "./rolldown-runtime-BUUfjzRC.m
2
2
  import { SeedcordError, validateDiscordToken } from "@seedcord/errors/internal";
3
3
  import { Converters, Envapter } from "envapt";
4
4
  import { DiscordAPIError, REST } from "@discordjs/rest";
5
- import { BaseHandler, DispatchContext, Fault, Notice, RepliableHandler, Silence } from "@seedcord/core";
6
- import { BaseReplySender, GatedMetadataKey, decodeComponentRoute, deferFlags, prefixOf, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
5
+ import { BaseHandler, Bus, DispatchContext, Fault, Notice, RepliableHandler, Silence, Subscriber, WebhookLog } from "@seedcord/core";
6
+ import { BaseReplySender, FaultThrottle, GatedMetadataKey, PublishDefault, asError, decodeComponentRoute, deferFlags, dispatchedPayload, outcomeFor, prefixOf, queuedMsFor, reportedWrite, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
7
7
  import { Logger, paint } from "@seedcord/logger";
8
8
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
9
9
  import { ApplicationCommandOptionType, ApplicationCommandType, ComponentType, InteractionResponseType, InteractionType, MessageFlags, RESTJSONErrorCodes, Routes } from "discord-api-types/v10";
10
10
  import { API } from "@discordjs/core/http-only";
11
- import { SeedcordErrorCode } from "@seedcord/errors";
12
11
  import { buildSlashRoute } from "@seedcord/utils/internal";
12
+ import { SeedcordErrorCode } from "@seedcord/errors";
13
13
 
14
14
  //#region src/reply/ReplySender.ts
15
15
  function wireAllowedMentions(mentions) {
@@ -43,8 +43,11 @@ function callbackData(reply, flags) {
43
43
  var ReplySender = class extends BaseReplySender {
44
44
  ref;
45
45
  rest;
46
- constructor(ref, rest, routeId) {
47
- super(routeId);
46
+ constructor(ref, rest, routeId, bus) {
47
+ super(routeId, "unacked", bus && {
48
+ bus,
49
+ interactionId: ref.id
50
+ });
48
51
  this.ref = ref;
49
52
  this.rest = rest;
50
53
  }
@@ -145,7 +148,7 @@ var RepliableHandler$1 = class extends RepliableHandler {
145
148
  application_id: event.application_id,
146
149
  id: event.id,
147
150
  token: event.token
148
- }, core.rest, routeId);
151
+ }, core.rest, routeId, core.bus);
149
152
  }
150
153
  /**
151
154
  * Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
@@ -178,31 +181,132 @@ function interactionGateContext(payload, core, routeId) {
178
181
  };
179
182
  }
180
183
 
184
+ //#endregion
185
+ //#region src/dispatch/slashRouteOf.ts
186
+ /**
187
+ * Computes the dispatch route for a chat-input or autocomplete payload, the raw-payload port of the
188
+ * gateway's `slashRouteOf`. Both assemble keys through `buildSlashRoute`, so a dispatched interaction
189
+ * and a generated registry key always match.
190
+ */
191
+ function slashRouteOf(data) {
192
+ const first = data.options?.[0];
193
+ if (first?.type === ApplicationCommandOptionType.SubcommandGroup) return buildSlashRoute(data.name, first.options[0]?.name, first.name);
194
+ if (first?.type === ApplicationCommandOptionType.Subcommand) return buildSlashRoute(data.name, first.name);
195
+ return data.name;
196
+ }
197
+
198
+ //#endregion
199
+ //#region src/dispatch/reportFault.ts
200
+ let faultLogger;
201
+ function logger$1() {
202
+ faultLogger ??= new Logger("Faults", { channel: "errors" });
203
+ return faultLogger;
204
+ }
205
+ /**
206
+ * Publishes a fault on the bus, the raw-payload equivalent of the reporting half of gateway's
207
+ * `extractErrorResponse`. A reported {@link Notice} publishes `handledException`, everything else
208
+ * publishes `unknownException`.
209
+ *
210
+ * @internal
211
+ */
212
+ function reportFault(error, uuid, payload, core) {
213
+ const throttle = FaultThrottle.for(core);
214
+ const key = `${routeKeyOf(payload)}:${nameOf(error)}`;
215
+ if (!throttle.shouldReport(key)) {
216
+ logger$1().debug(`throttled duplicate fault ${paint.mute(key)}`);
217
+ return;
218
+ }
219
+ const source = interactionSource(payload);
220
+ if (error instanceof Notice && source) core.bus[PublishDefault]("handledException", {
221
+ denial: error,
222
+ uuid,
223
+ source
224
+ });
225
+ else core.bus[PublishDefault]("unknownException", {
226
+ uuid,
227
+ error,
228
+ ...actors(payload),
229
+ metadata: payload
230
+ });
231
+ throttle.markReported(key);
232
+ }
233
+ function routeKeyOf(payload) {
234
+ if (payload.type === InteractionType.ApplicationCommandAutocomplete) return `autocomplete:${slashRouteOf(payload.data)}`;
235
+ if (payload.type === InteractionType.ApplicationCommand) return payload.data.type === ApplicationCommandType.ChatInput ? slashRouteOf(payload.data) : payload.data.name;
236
+ return prefixOf(payload.data.custom_id) || payload.data.custom_id;
237
+ }
238
+ function nameOf(error) {
239
+ if (error instanceof Notice) return error.name;
240
+ if (error instanceof DiscordAPIError) return String(error.code);
241
+ return error.constructor.name;
242
+ }
243
+ function actors(payload) {
244
+ const user = payload.member?.user ?? payload.user;
245
+ return {
246
+ guild: void 0,
247
+ user: user ? {
248
+ id: user.id,
249
+ username: user.username
250
+ } : void 0
251
+ };
252
+ }
253
+ function interactionSource(payload) {
254
+ const user = payload.member?.user ?? payload.user;
255
+ if (!user) return null;
256
+ const common = {
257
+ kind: "interaction",
258
+ userId: user.id,
259
+ guildId: payload.guild_id ?? null,
260
+ channelId: payload.channel?.id ?? null,
261
+ interactionId: payload.id,
262
+ raw: payload
263
+ };
264
+ if (payload.type === InteractionType.ApplicationCommand) {
265
+ const isSlash = payload.data.type === ApplicationCommandType.ChatInput;
266
+ return {
267
+ ...common,
268
+ interactionKind: isSlash ? "slash" : "context-menu",
269
+ command: routeKeyOf(payload),
270
+ customId: null
271
+ };
272
+ }
273
+ if (payload.type === InteractionType.ModalSubmit) return {
274
+ ...common,
275
+ interactionKind: "modal",
276
+ command: null,
277
+ customId: payload.data.custom_id
278
+ };
279
+ if (payload.type === InteractionType.MessageComponent) {
280
+ const isButton = payload.data.component_type === ComponentType.Button;
281
+ return {
282
+ ...common,
283
+ interactionKind: isButton ? "button" : "select",
284
+ command: null,
285
+ customId: payload.data.custom_id
286
+ };
287
+ }
288
+ return null;
289
+ }
290
+
181
291
  //#endregion
182
292
  //#region src/dispatch/dispatchInteraction.ts
183
293
  let dispatchLogger;
184
294
  function logger() {
185
- dispatchLogger ??= new Logger("Dispatcher");
295
+ dispatchLogger ??= new Logger("Dispatcher", { channel: "interactions" });
186
296
  return dispatchLogger;
187
297
  }
188
298
  function createCore(config, token) {
189
- return {
299
+ const draft = {
190
300
  config,
191
301
  rateLimiter: config.store ?? new MemoryRateLimiter(),
192
- rest: new REST().setToken(token),
193
- version: "0.1.0-next.1",
194
- username: void 0,
195
- augmentTarget: "@seedcord/http"
302
+ rest: new REST().setToken(token)
196
303
  };
304
+ draft.bus = new Bus(draft);
305
+ return draft;
197
306
  }
198
307
  function isHandlerCtor(value) {
199
308
  return typeof value === "function" && value.prototype instanceof BaseHandler;
200
309
  }
201
- function handlerCtorOf(moduleExports) {
202
- if (typeof moduleExports !== "object" || moduleExports === null) return null;
203
- for (const value of Object.values(moduleExports)) if (isHandlerCtor(value)) return value;
204
- return null;
205
- }
206
310
  const HARMLESS_API_CODES = new Set([
207
311
  RESTJSONErrorCodes.UnknownInteraction,
208
312
  RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
@@ -220,10 +324,13 @@ async function sendGuarded(routeId, send) {
220
324
  }
221
325
  }
222
326
  async function respondEmptyChoices(scope) {
223
- await scope.core.rest.post(Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
327
+ await reportedWrite({
328
+ bus: scope.core.bus,
329
+ interactionId: scope.payload.id
330
+ }, scope.routeId, "respond", () => scope.core.rest.post(Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
224
331
  type: InteractionResponseType.ApplicationCommandAutocompleteResult,
225
332
  data: { choices: [] }
226
- } });
333
+ } }));
227
334
  }
228
335
  function renderContext(core, uuid) {
229
336
  const developerUsername = core.config.notifications?.developerUsername;
@@ -233,7 +340,10 @@ function renderContext(core, uuid) {
233
340
  };
234
341
  }
235
342
  async function handleNotice(notice, uuid, scope) {
236
- if (notice.report) logger().error(`${notice.name}: ${paint.mute(uuid)}`, notice);
343
+ if (notice.report) {
344
+ logger().error(`${notice.name}: ${paint.mute(uuid)}`, notice);
345
+ reportFault(notice, uuid, scope.payload, scope.core);
346
+ }
237
347
  const { sender } = scope;
238
348
  if (!sender) {
239
349
  await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
@@ -246,6 +356,7 @@ async function handleRawFault(error, uuid, scope) {
246
356
  const { core, sender } = scope;
247
357
  if (core.config.errors?.errorStack ?? false) logger().error(paint.mute(uuid), error);
248
358
  else logger().error(`${paint.mute(uuid)} | ${error.message}`);
359
+ reportFault(error, uuid, scope.payload, core);
249
360
  if (!sender) {
250
361
  await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
251
362
  return;
@@ -264,7 +375,7 @@ async function handleFault(caught, scope) {
264
375
  await handleNotice(caught, uuid, scope);
265
376
  return;
266
377
  }
267
- const error = Error.isError(caught) ? caught : new Error(String(caught));
378
+ const error = asError(caught);
268
379
  const ignore = new Set(scope.core.config.errors?.ignoreApiCodes ?? []);
269
380
  if (error instanceof DiscordAPIError && ignore.has(error.code)) {
270
381
  logger().debug(`swallowed api code ${paint.amber(String(error.code))}`);
@@ -272,79 +383,149 @@ async function handleFault(caught, scope) {
272
383
  }
273
384
  await handleRawFault(error, uuid, scope);
274
385
  }
386
+ function unhandledRouteId(match) {
387
+ if (match.routeId) return match.routeId;
388
+ const key = match.attemptedKey ?? "";
389
+ return `${match.kind}:${key.length > 0 ? key : "unrouted"}`;
390
+ }
275
391
  function freshScope(match, payload, core) {
276
392
  const ref = {
277
393
  application_id: payload.application_id,
278
394
  id: payload.id,
279
395
  token: payload.token
280
396
  };
281
- const routeId = match.routeId ?? "unhandled";
397
+ const routeId = unhandledRouteId(match);
282
398
  return {
283
399
  core,
284
400
  payload,
285
401
  routeId,
286
- sender: match.kind === "autocomplete" ? null : new ReplySender(ref, core.rest, routeId)
402
+ sender: match.kind === "autocomplete" ? null : new ReplySender(ref, core.rest, routeId, core.bus)
287
403
  };
288
404
  }
405
+ function dispatchReporter(match, payload, core) {
406
+ const startedAt = performance.now();
407
+ const queuedMs = queuedMsFor(payload.id);
408
+ return (outcome) => {
409
+ core.bus[PublishDefault]("interactionDispatched", dispatchedPayload({
410
+ routeId: unhandledRouteId(match),
411
+ interactionId: payload.id,
412
+ kind: match.kind,
413
+ outcome,
414
+ fallback: match.routeId === null,
415
+ startedAt,
416
+ queuedMs
417
+ }));
418
+ };
419
+ }
420
+ async function answer(caught, scope, report, outcome = outcomeFor(caught)) {
421
+ try {
422
+ await handleFault(caught, scope);
423
+ } finally {
424
+ report(outcome);
425
+ }
426
+ }
427
+ async function loadHandlerCtor(match, payload, core, report) {
428
+ const routeId = unhandledRouteId(match);
429
+ let exported;
430
+ try {
431
+ exported = await match.load();
432
+ } catch (caught) {
433
+ logger().error(`Route ${paint.sky.bold(routeId)} failed to load its handler.`, caught);
434
+ await answer(caught, freshScope(match, payload, core), report, "failed");
435
+ return null;
436
+ }
437
+ if (isHandlerCtor(exported)) return exported;
438
+ logger().error(`Route ${paint.sky.bold(routeId)} loaded an export that is not a handler class.`);
439
+ await answer(/* @__PURE__ */ new Error(`route ${routeId} loaded an export that is not a handler class`), freshScope(match, payload, core), report, "failed");
440
+ return null;
441
+ }
289
442
  /**
290
- * Runs the pre-ack phase for a matched interaction. Loads the route module, constructs the handler, and
443
+ * Runs the pre-ack phase for a matched interaction. Loads the route's handler class, constructs it, and
291
444
  * runs its gates, and the sender posts any refusal. Returns the post-ack execute continuation, or null
292
445
  * when a gate refuses or nothing can run.
293
446
  */
294
447
  async function dispatchInteraction(args) {
295
448
  const { match, payload, core } = args;
296
- let moduleExports;
297
- try {
298
- moduleExports = await match.load();
299
- } catch (caught) {
300
- logger().error(`Route ${paint.sky.bold(match.routeId ?? "unhandled")} failed to load its module.`, caught);
301
- await handleFault(caught, freshScope(match, payload, core));
302
- return null;
303
- }
304
- const ctor = handlerCtorOf(moduleExports);
305
- if (!ctor) {
306
- const routeId = match.routeId ?? "unhandled";
307
- logger().error(`Route ${paint.sky.bold(routeId)} loaded a module with no handler class.`);
308
- await handleFault(/* @__PURE__ */ new Error(`route ${routeId} loaded a module with no handler class`), freshScope(match, payload, core));
309
- return null;
310
- }
311
- const dispatch = new DispatchContext(match.routeId);
449
+ const report = dispatchReporter(match, payload, core);
450
+ const ctor = await loadHandlerCtor(match, payload, core, report);
451
+ if (!ctor) return null;
452
+ const dispatch = new DispatchContext(unhandledRouteId(match));
312
453
  let handler;
313
454
  try {
314
455
  handler = new ctor(payload, core, dispatch);
315
456
  } catch (caught) {
316
- await handleFault(caught, freshScope(match, payload, core));
457
+ await answer(caught, freshScope(match, payload, core), report);
317
458
  return null;
318
459
  }
319
460
  const scope = {
320
461
  core,
321
462
  payload,
322
- routeId: match.routeId ?? handler.constructor.name,
463
+ routeId: unhandledRouteId(match),
323
464
  sender: handler instanceof RepliableHandler$1 ? handler.getSender() : null
324
465
  };
325
- if (!await passedGates(ctor, match, payload, core, scope)) return null;
466
+ const refusal = await gateRefusal(ctor, match, payload, core);
467
+ if (refusal) {
468
+ await answer(refusal.caught, scope, report);
469
+ return null;
470
+ }
326
471
  return async () => {
327
472
  try {
328
473
  await handler.execute();
474
+ report("handled");
329
475
  } catch (caught) {
330
- await handleFault(caught, scope);
476
+ await answer(caught, scope, report);
331
477
  }
332
478
  };
333
479
  }
334
- async function passedGates(ctor, match, payload, core, scope) {
335
- if (match.kind === "autocomplete" || payload.type === InteractionType.ApplicationCommandAutocomplete) return true;
480
+ async function gateRefusal(ctor, match, payload, core) {
481
+ if (match.kind === "autocomplete" || payload.type === InteractionType.ApplicationCommandAutocomplete) return null;
336
482
  const monitor = slowGateMonitor();
337
483
  try {
338
484
  await runHandlerGates(ctor, interactionGateContext(payload, core, match.routeId), match.routeId ?? void 0, monitor?.observe);
339
- return true;
485
+ return null;
340
486
  } catch (caught) {
341
- await handleFault(caught, scope);
342
- return false;
487
+ return { caught };
343
488
  } finally {
344
489
  monitor?.report(match.routeId);
345
490
  }
346
491
  }
347
492
 
493
+ //#endregion
494
+ //#region src/dispatch/registerSubscribers.ts
495
+ /**
496
+ * Registers a manifest's subscribers on the bus without importing them. Each row's module loads on
497
+ * the first publish of one of its keys, which keeps an isolate that never publishes free of the cost.
498
+ *
499
+ * @internal
500
+ */
501
+ function registerSubscribers(bus, manifest) {
502
+ for (const row of manifest.subscriberRoutes) bus.register({
503
+ keys: row.keys,
504
+ frequency: row.frequency,
505
+ resolve: async () => {
506
+ const keys = row.keys.join(", ");
507
+ let module;
508
+ try {
509
+ module = await row.load();
510
+ } catch (caught) {
511
+ throw new SeedcordError(SeedcordErrorCode.RouteModuleLoadFailed, [keys, row.from], { cause: caught });
512
+ }
513
+ if (!Object.hasOwn(module, row.exportName)) throw new SeedcordError(SeedcordErrorCode.InteractionRouteExportMissing, [
514
+ keys,
515
+ row.exportName,
516
+ row.from
517
+ ]);
518
+ const exported = module[row.exportName];
519
+ if (typeof exported !== "function" || !(exported.prototype instanceof Subscriber)) throw new SeedcordError(SeedcordErrorCode.SubscriberRouteNotASubscriber, [
520
+ keys,
521
+ row.exportName,
522
+ row.from
523
+ ]);
524
+ return exported;
525
+ }
526
+ });
527
+ }
528
+
348
529
  //#endregion
349
530
  //#region src/inputs/HttpSlashOptions.ts
350
531
  var HttpSlashOptions = class {
@@ -421,20 +602,6 @@ function numeric(value) {
421
602
  return Number.isNaN(parsed) ? null : parsed;
422
603
  }
423
604
 
424
- //#endregion
425
- //#region src/dispatch/slashRouteOf.ts
426
- /**
427
- * Computes the dispatch route for a chat-input or autocomplete payload, the raw-payload port of the
428
- * gateway's `slashRouteOf`. Both assemble keys through `buildSlashRoute`, so a dispatched interaction
429
- * and a generated registry key always match.
430
- */
431
- function slashRouteOf(data) {
432
- const first = data.options?.[0];
433
- if (first?.type === ApplicationCommandOptionType.SubcommandGroup) return buildSlashRoute(data.name, first.options[0]?.name, first.name);
434
- if (first?.type === ApplicationCommandOptionType.Subcommand) return buildSlashRoute(data.name, first.name);
435
- return data.name;
436
- }
437
-
438
605
  //#endregion
439
606
  //#region src/handlers/interaction/AutocompleteHandler.ts
440
607
  /**
@@ -448,7 +615,7 @@ function slashRouteOf(data) {
448
615
  */
449
616
  var AutocompleteHandler = class extends BaseHandler {
450
617
  constructor(event, core, dispatch) {
451
- super(event, core, dispatch);
618
+ super(event, core, dispatch, "interactions");
452
619
  }
453
620
  /**
454
621
  * Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
@@ -491,10 +658,14 @@ var AutocompleteHandler = class extends BaseHandler {
491
658
  }
492
659
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
493
660
  async respond(choices) {
494
- await this.core.rest.post(Routes.interactionCallback(this.event.id, this.event.token), { body: {
661
+ const routeId = this.dispatch?.routeId ?? "autocomplete";
662
+ await reportedWrite({
663
+ bus: this.core.bus,
664
+ interactionId: this.event.id
665
+ }, routeId, "respond", () => this.core.rest.post(Routes.interactionCallback(this.event.id, this.event.token), { body: {
495
666
  type: InteractionResponseType.ApplicationCommandAutocompleteResult,
496
667
  data: { choices }
497
- } });
668
+ } }));
498
669
  }
499
670
  /** The firing command route, for a field whose completion differs per registered command. */
500
671
  get route() {
@@ -592,14 +763,30 @@ function componentMapKey(type) {
592
763
  default: return null;
593
764
  }
594
765
  }
595
- function set(map, kind, key, load) {
596
- map.set(key, {
597
- kind,
598
- routeId: `${kind}:${key}`,
599
- load
600
- });
766
+ function namedExport(routeId, row) {
767
+ return async () => {
768
+ let moduleExports;
769
+ try {
770
+ moduleExports = await row.load();
771
+ } catch (caught) {
772
+ throw new SeedcordError(SeedcordErrorCode.RouteModuleLoadFailed, [routeId, row.from], { cause: caught });
773
+ }
774
+ if (!Object.hasOwn(moduleExports, row.exportName)) throw new SeedcordError(SeedcordErrorCode.InteractionRouteExportMissing, [
775
+ routeId,
776
+ row.exportName,
777
+ row.from
778
+ ]);
779
+ return moduleExports[row.exportName];
780
+ };
601
781
  }
602
- /** Builds the per-kind lookup maps. */
782
+ function describeRow(row) {
783
+ return `${row.exportName} (${row.from})`;
784
+ }
785
+ /**
786
+ * Builds the per-kind lookup maps.
787
+ *
788
+ * @throws A **SeedcordError** when two rows resolve to the same route.
789
+ */
603
790
  function buildRouteMaps(manifest) {
604
791
  const maps = {
605
792
  slash: /* @__PURE__ */ new Map(),
@@ -616,19 +803,36 @@ function buildRouteMaps(manifest) {
616
803
  modal: /* @__PURE__ */ new Map()
617
804
  }
618
805
  };
619
- for (const row of manifest.commands) {
806
+ const owners = /* @__PURE__ */ new Map();
807
+ function set(map, kind, key, row) {
808
+ const routeId = `${kind}:${key}`;
809
+ const owner = owners.get(routeId);
810
+ if (owner) throw new SeedcordError(SeedcordErrorCode.InteractionDuplicateRoute, [
811
+ routeId,
812
+ describeRow(owner),
813
+ describeRow(row)
814
+ ]);
815
+ owners.set(routeId, row);
816
+ map.set(key, {
817
+ kind,
818
+ routeId,
819
+ load: namedExport(routeId, row)
820
+ });
821
+ }
822
+ for (const row of manifest.commandRoutes) {
620
823
  const kind = commandKind(row.type);
621
- if (kind) set(maps[kind], kind, row.name, row.load);
824
+ if (kind) set(maps[kind], kind, row.name, row);
622
825
  }
623
- for (const row of manifest.autocomplete) set(maps.autocomplete, "autocomplete", row.name, row.load);
624
- for (const row of manifest.components) set(maps.components[row.kind], COMPONENT_KIND[row.kind], row.prefix, row.load);
826
+ for (const row of manifest.autocompleteRoutes) set(maps.autocomplete, "autocomplete", row.name, row);
827
+ for (const row of manifest.componentRoutes) set(maps.components[row.kind], COMPONENT_KIND[row.kind], row.prefix, row);
625
828
  return maps;
626
829
  }
627
- function unhandled(kind) {
830
+ function unhandled(kind, attemptedKey) {
628
831
  return {
629
832
  kind,
630
833
  routeId: null,
631
- load: () => Promise.resolve(kind === "autocomplete" ? { UnhandledAutocomplete } : { UnhandledRepliable })
834
+ attemptedKey,
835
+ load: () => Promise.resolve(kind === "autocomplete" ? UnhandledAutocomplete : UnhandledRepliable)
632
836
  };
633
837
  }
634
838
  /**
@@ -641,17 +845,27 @@ function unhandled(kind) {
641
845
  function resolve(maps, interaction) {
642
846
  switch (interaction.type) {
643
847
  case InteractionType.ApplicationCommand: {
644
- if (interaction.data.type === ApplicationCommandType.ChatInput) return maps.slash.get(slashRouteOf(interaction.data)) ?? unhandled("slash");
848
+ if (interaction.data.type === ApplicationCommandType.ChatInput) {
849
+ const route = slashRouteOf(interaction.data);
850
+ return maps.slash.get(route) ?? unhandled("slash", route);
851
+ }
645
852
  const kind = commandKind(interaction.data.type);
646
- return kind ? maps[kind].get(interaction.data.name) ?? unhandled(kind) : null;
853
+ return kind ? maps[kind].get(interaction.data.name) ?? unhandled(kind, interaction.data.name) : null;
854
+ }
855
+ case InteractionType.ApplicationCommandAutocomplete: {
856
+ const route = slashRouteOf(interaction.data);
857
+ return maps.autocomplete.get(route) ?? unhandled("autocomplete", route);
647
858
  }
648
- case InteractionType.ApplicationCommandAutocomplete: return maps.autocomplete.get(slashRouteOf(interaction.data)) ?? unhandled("autocomplete");
649
859
  case InteractionType.MessageComponent: {
650
860
  const key = componentMapKey(interaction.data.component_type);
651
861
  if (!key) return null;
652
- return maps.components[key].get(prefixOf(interaction.data.custom_id)) ?? unhandled(COMPONENT_KIND[key]);
862
+ const prefix = prefixOf(interaction.data.custom_id);
863
+ return maps.components[key].get(prefix) ?? unhandled(COMPONENT_KIND[key], prefix);
864
+ }
865
+ case InteractionType.ModalSubmit: {
866
+ const prefix = prefixOf(interaction.data.custom_id);
867
+ return maps.components.modal.get(prefix) ?? unhandled("modal", prefix);
653
868
  }
654
- case InteractionType.ModalSubmit: return maps.components.modal.get(prefixOf(interaction.data.custom_id)) ?? unhandled("modal");
655
869
  default: return null;
656
870
  }
657
871
  }
@@ -732,7 +946,7 @@ function buildEngine(core, maps) {
732
946
  const verifier = new Ed25519Verifier(Envapter.getRequired("DISCORD_PUBLIC_KEY", Converters.String));
733
947
  const replays = new ReplayGuard();
734
948
  const inFlight = /* @__PURE__ */ new Set();
735
- const logger = new Logger("Engine");
949
+ const logger = new Logger("Engine", { channel: "bot" });
736
950
  async function dispatchMatched(match, payload, ctx) {
737
951
  const start = await dispatchInteraction({
738
952
  match,
@@ -740,13 +954,18 @@ function buildEngine(core, maps) {
740
954
  core
741
955
  });
742
956
  if (!start) return;
743
- const work = start();
957
+ const work = start().catch(rootFault);
744
958
  if (ctx) ctx.waitUntil(work);
745
959
  else {
746
960
  inFlight.add(work);
747
961
  work.finally(() => inFlight.delete(work));
748
962
  }
749
963
  }
964
+ function rootFault(caught) {
965
+ const error = asError(caught);
966
+ logger.error("dispatch failed past the boundary", error);
967
+ core.bus[PublishDefault]("unhandledInteractionError", { error });
968
+ }
750
969
  const handle = async (request, ctx) => {
751
970
  if (request.method !== "POST") return new Response(null, {
752
971
  status: METHOD_NOT_ALLOWED,
@@ -767,10 +986,11 @@ function buildEngine(core, maps) {
767
986
  if (!hasInteractionType(payload)) return new Response(null, { status: BAD_REQUEST });
768
987
  if (payload.type === PING) return Response.json({ type: InteractionResponseType.Pong });
769
988
  try {
989
+ core.bus[PublishDefault]("anyInteraction", { interaction: payload });
770
990
  const match = resolve(maps, payload);
771
991
  if (match) await dispatchMatched(match, payload, ctx);
772
992
  } catch (caught) {
773
- logger.error("dispatch failed, acking anyway", caught);
993
+ rootFault(caught);
774
994
  }
775
995
  return new Response(null, { status: ACCEPTED });
776
996
  };
@@ -796,7 +1016,10 @@ function buildEngine(core, maps) {
796
1016
  * `SeedcordError` when either is missing or malformed.
797
1017
  */
798
1018
  function createSeedcord(config, manifest) {
799
- return buildEngine(createCore(config, validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN"))), buildRouteMaps(manifest)).handle;
1019
+ const core = createCore(config, validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN")));
1020
+ core.bus.registerDefaults();
1021
+ registerSubscribers(core.bus, manifest);
1022
+ return buildEngine(core, buildRouteMaps(manifest)).handle;
800
1023
  }
801
1024
 
802
1025
  //#endregion
@@ -995,6 +1218,22 @@ var ModalHandler = class extends ComponentHandler {
995
1218
  }
996
1219
  };
997
1220
 
1221
+ //#endregion
1222
+ //#region src/subscribers/index.ts
1223
+ /**
1224
+ * Base class for an HTTP subscriber, binding `this.core` to the HTTP {@link Core}.
1225
+ *
1226
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
1227
+ */
1228
+ var Subscriber$1 = class extends Subscriber {};
1229
+ /**
1230
+ * Base class for an HTTP subscriber that delivers its event to a Discord webhook, binding
1231
+ * `this.core` to the HTTP {@link Core}.
1232
+ *
1233
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1234
+ */
1235
+ var WebhookLog$1 = class extends WebhookLog {};
1236
+
998
1237
  //#endregion
999
1238
  //#region src/gates/Gated.ts
1000
1239
  /**
@@ -1039,8 +1278,8 @@ function Gated(...gates) {
1039
1278
  //#endregion
1040
1279
  //#region src/version.ts
1041
1280
  /** Package version */
1042
- const version = "0.1.0-next.1";
1281
+ const version = "0.1.0-next.2";
1043
1282
 
1044
1283
  //#endregion
1045
- export { ButtonHandler as a, SlashHandler as c, buildRouteMaps as d, InteractionHandler as f, SelectMenuHandler as i, createSeedcord as l, RepliableHandler$1 as m, Gated as n, ComponentHandler as o, AutocompleteHandler as p, ModalHandler as r, ContextMenuHandler as s, version as t, buildEngine as u };
1046
- //# sourceMappingURL=edge.index-BAgNb6TV.mjs.map
1284
+ export { ModalHandler as a, ComponentHandler as c, createSeedcord as d, buildEngine as f, RepliableHandler$1 as g, AutocompleteHandler as h, WebhookLog$1 as i, ContextMenuHandler as l, InteractionHandler as m, Gated as n, SelectMenuHandler as o, buildRouteMaps as p, Subscriber$1 as r, ButtonHandler as s, version as t, SlashHandler as u };
1285
+ //# sourceMappingURL=edge.index-BBRnxxkS.mjs.map