@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.
@@ -42,8 +42,8 @@ let _seedcord_logger = require("@seedcord/logger");
42
42
  let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
43
43
  let discord_api_types_v10 = require("discord-api-types/v10");
44
44
  let _discordjs_core_http_only = require("@discordjs/core/http-only");
45
- let _seedcord_errors = require("@seedcord/errors");
46
45
  let _seedcord_utils_internal = require("@seedcord/utils/internal");
46
+ let _seedcord_errors = require("@seedcord/errors");
47
47
 
48
48
  //#region src/reply/ReplySender.ts
49
49
  function wireAllowedMentions(mentions) {
@@ -77,8 +77,11 @@ function callbackData(reply, flags) {
77
77
  var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
78
78
  ref;
79
79
  rest;
80
- constructor(ref, rest, routeId) {
81
- super(routeId);
80
+ constructor(ref, rest, routeId, bus) {
81
+ super(routeId, "unacked", bus && {
82
+ bus,
83
+ interactionId: ref.id
84
+ });
82
85
  this.ref = ref;
83
86
  this.rest = rest;
84
87
  }
@@ -179,7 +182,7 @@ var RepliableHandler = class extends _seedcord_core.RepliableHandler {
179
182
  application_id: event.application_id,
180
183
  id: event.id,
181
184
  token: event.token
182
- }, core.rest, routeId);
185
+ }, core.rest, routeId, core.bus);
183
186
  }
184
187
  /**
185
188
  * Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
@@ -212,31 +215,132 @@ function interactionGateContext(payload, core, routeId) {
212
215
  };
213
216
  }
214
217
 
218
+ //#endregion
219
+ //#region src/dispatch/slashRouteOf.ts
220
+ /**
221
+ * Computes the dispatch route for a chat-input or autocomplete payload, the raw-payload port of the
222
+ * gateway's `slashRouteOf`. Both assemble keys through `buildSlashRoute`, so a dispatched interaction
223
+ * and a generated registry key always match.
224
+ */
225
+ function slashRouteOf(data) {
226
+ const first = data.options?.[0];
227
+ if (first?.type === discord_api_types_v10.ApplicationCommandOptionType.SubcommandGroup) return (0, _seedcord_utils_internal.buildSlashRoute)(data.name, first.options[0]?.name, first.name);
228
+ if (first?.type === discord_api_types_v10.ApplicationCommandOptionType.Subcommand) return (0, _seedcord_utils_internal.buildSlashRoute)(data.name, first.name);
229
+ return data.name;
230
+ }
231
+
232
+ //#endregion
233
+ //#region src/dispatch/reportFault.ts
234
+ let faultLogger;
235
+ function logger$1() {
236
+ faultLogger ??= new _seedcord_logger.Logger("Faults", { channel: "errors" });
237
+ return faultLogger;
238
+ }
239
+ /**
240
+ * Publishes a fault on the bus, the raw-payload equivalent of the reporting half of gateway's
241
+ * `extractErrorResponse`. A reported {@link Notice} publishes `handledException`, everything else
242
+ * publishes `unknownException`.
243
+ *
244
+ * @internal
245
+ */
246
+ function reportFault(error, uuid, payload, core) {
247
+ const throttle = _seedcord_core_internal.FaultThrottle.for(core);
248
+ const key = `${routeKeyOf(payload)}:${nameOf(error)}`;
249
+ if (!throttle.shouldReport(key)) {
250
+ logger$1().debug(`throttled duplicate fault ${_seedcord_logger.paint.mute(key)}`);
251
+ return;
252
+ }
253
+ const source = interactionSource(payload);
254
+ if (error instanceof _seedcord_core.Notice && source) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
255
+ denial: error,
256
+ uuid,
257
+ source
258
+ });
259
+ else core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
260
+ uuid,
261
+ error,
262
+ ...actors(payload),
263
+ metadata: payload
264
+ });
265
+ throttle.markReported(key);
266
+ }
267
+ function routeKeyOf(payload) {
268
+ if (payload.type === discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete) return `autocomplete:${slashRouteOf(payload.data)}`;
269
+ if (payload.type === discord_api_types_v10.InteractionType.ApplicationCommand) return payload.data.type === discord_api_types_v10.ApplicationCommandType.ChatInput ? slashRouteOf(payload.data) : payload.data.name;
270
+ return (0, _seedcord_core_internal.prefixOf)(payload.data.custom_id) || payload.data.custom_id;
271
+ }
272
+ function nameOf(error) {
273
+ if (error instanceof _seedcord_core.Notice) return error.name;
274
+ if (error instanceof _discordjs_rest.DiscordAPIError) return String(error.code);
275
+ return error.constructor.name;
276
+ }
277
+ function actors(payload) {
278
+ const user = payload.member?.user ?? payload.user;
279
+ return {
280
+ guild: void 0,
281
+ user: user ? {
282
+ id: user.id,
283
+ username: user.username
284
+ } : void 0
285
+ };
286
+ }
287
+ function interactionSource(payload) {
288
+ const user = payload.member?.user ?? payload.user;
289
+ if (!user) return null;
290
+ const common = {
291
+ kind: "interaction",
292
+ userId: user.id,
293
+ guildId: payload.guild_id ?? null,
294
+ channelId: payload.channel?.id ?? null,
295
+ interactionId: payload.id,
296
+ raw: payload
297
+ };
298
+ if (payload.type === discord_api_types_v10.InteractionType.ApplicationCommand) {
299
+ const isSlash = payload.data.type === discord_api_types_v10.ApplicationCommandType.ChatInput;
300
+ return {
301
+ ...common,
302
+ interactionKind: isSlash ? "slash" : "context-menu",
303
+ command: routeKeyOf(payload),
304
+ customId: null
305
+ };
306
+ }
307
+ if (payload.type === discord_api_types_v10.InteractionType.ModalSubmit) return {
308
+ ...common,
309
+ interactionKind: "modal",
310
+ command: null,
311
+ customId: payload.data.custom_id
312
+ };
313
+ if (payload.type === discord_api_types_v10.InteractionType.MessageComponent) {
314
+ const isButton = payload.data.component_type === discord_api_types_v10.ComponentType.Button;
315
+ return {
316
+ ...common,
317
+ interactionKind: isButton ? "button" : "select",
318
+ command: null,
319
+ customId: payload.data.custom_id
320
+ };
321
+ }
322
+ return null;
323
+ }
324
+
215
325
  //#endregion
216
326
  //#region src/dispatch/dispatchInteraction.ts
217
327
  let dispatchLogger;
218
328
  function logger() {
219
- dispatchLogger ??= new _seedcord_logger.Logger("Dispatcher");
329
+ dispatchLogger ??= new _seedcord_logger.Logger("Dispatcher", { channel: "interactions" });
220
330
  return dispatchLogger;
221
331
  }
222
332
  function createCore(config, token) {
223
- return {
333
+ const draft = {
224
334
  config,
225
335
  rateLimiter: config.store ?? new _seedcord_rate_limiter.MemoryRateLimiter(),
226
- rest: new _discordjs_rest.REST().setToken(token),
227
- version: "0.1.0-next.1",
228
- username: void 0,
229
- augmentTarget: "@seedcord/http"
336
+ rest: new _discordjs_rest.REST().setToken(token)
230
337
  };
338
+ draft.bus = new _seedcord_core.Bus(draft);
339
+ return draft;
231
340
  }
232
341
  function isHandlerCtor(value) {
233
342
  return typeof value === "function" && value.prototype instanceof _seedcord_core.BaseHandler;
234
343
  }
235
- function handlerCtorOf(moduleExports) {
236
- if (typeof moduleExports !== "object" || moduleExports === null) return null;
237
- for (const value of Object.values(moduleExports)) if (isHandlerCtor(value)) return value;
238
- return null;
239
- }
240
344
  const HARMLESS_API_CODES = new Set([
241
345
  discord_api_types_v10.RESTJSONErrorCodes.UnknownInteraction,
242
346
  discord_api_types_v10.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
@@ -254,10 +358,13 @@ async function sendGuarded(routeId, send) {
254
358
  }
255
359
  }
256
360
  async function respondEmptyChoices(scope) {
257
- await scope.core.rest.post(discord_api_types_v10.Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
361
+ await (0, _seedcord_core_internal.reportedWrite)({
362
+ bus: scope.core.bus,
363
+ interactionId: scope.payload.id
364
+ }, scope.routeId, "respond", () => scope.core.rest.post(discord_api_types_v10.Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
258
365
  type: discord_api_types_v10.InteractionResponseType.ApplicationCommandAutocompleteResult,
259
366
  data: { choices: [] }
260
- } });
367
+ } }));
261
368
  }
262
369
  function renderContext(core, uuid) {
263
370
  const developerUsername = core.config.notifications?.developerUsername;
@@ -267,7 +374,10 @@ function renderContext(core, uuid) {
267
374
  };
268
375
  }
269
376
  async function handleNotice(notice, uuid, scope) {
270
- if (notice.report) logger().error(`${notice.name}: ${_seedcord_logger.paint.mute(uuid)}`, notice);
377
+ if (notice.report) {
378
+ logger().error(`${notice.name}: ${_seedcord_logger.paint.mute(uuid)}`, notice);
379
+ reportFault(notice, uuid, scope.payload, scope.core);
380
+ }
271
381
  const { sender } = scope;
272
382
  if (!sender) {
273
383
  await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
@@ -280,6 +390,7 @@ async function handleRawFault(error, uuid, scope) {
280
390
  const { core, sender } = scope;
281
391
  if (core.config.errors?.errorStack ?? false) logger().error(_seedcord_logger.paint.mute(uuid), error);
282
392
  else logger().error(`${_seedcord_logger.paint.mute(uuid)} | ${error.message}`);
393
+ reportFault(error, uuid, scope.payload, core);
283
394
  if (!sender) {
284
395
  await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
285
396
  return;
@@ -298,7 +409,7 @@ async function handleFault(caught, scope) {
298
409
  await handleNotice(caught, uuid, scope);
299
410
  return;
300
411
  }
301
- const error = Error.isError(caught) ? caught : new Error(String(caught));
412
+ const error = (0, _seedcord_core_internal.asError)(caught);
302
413
  const ignore = new Set(scope.core.config.errors?.ignoreApiCodes ?? []);
303
414
  if (error instanceof _discordjs_rest.DiscordAPIError && ignore.has(error.code)) {
304
415
  logger().debug(`swallowed api code ${_seedcord_logger.paint.amber(String(error.code))}`);
@@ -306,79 +417,149 @@ async function handleFault(caught, scope) {
306
417
  }
307
418
  await handleRawFault(error, uuid, scope);
308
419
  }
420
+ function unhandledRouteId(match) {
421
+ if (match.routeId) return match.routeId;
422
+ const key = match.attemptedKey ?? "";
423
+ return `${match.kind}:${key.length > 0 ? key : "unrouted"}`;
424
+ }
309
425
  function freshScope(match, payload, core) {
310
426
  const ref = {
311
427
  application_id: payload.application_id,
312
428
  id: payload.id,
313
429
  token: payload.token
314
430
  };
315
- const routeId = match.routeId ?? "unhandled";
431
+ const routeId = unhandledRouteId(match);
316
432
  return {
317
433
  core,
318
434
  payload,
319
435
  routeId,
320
- sender: match.kind === "autocomplete" ? null : new ReplySender(ref, core.rest, routeId)
436
+ sender: match.kind === "autocomplete" ? null : new ReplySender(ref, core.rest, routeId, core.bus)
321
437
  };
322
438
  }
439
+ function dispatchReporter(match, payload, core) {
440
+ const startedAt = performance.now();
441
+ const queuedMs = (0, _seedcord_core_internal.queuedMsFor)(payload.id);
442
+ return (outcome) => {
443
+ core.bus[_seedcord_core_internal.PublishDefault]("interactionDispatched", (0, _seedcord_core_internal.dispatchedPayload)({
444
+ routeId: unhandledRouteId(match),
445
+ interactionId: payload.id,
446
+ kind: match.kind,
447
+ outcome,
448
+ fallback: match.routeId === null,
449
+ startedAt,
450
+ queuedMs
451
+ }));
452
+ };
453
+ }
454
+ async function answer(caught, scope, report, outcome = (0, _seedcord_core_internal.outcomeFor)(caught)) {
455
+ try {
456
+ await handleFault(caught, scope);
457
+ } finally {
458
+ report(outcome);
459
+ }
460
+ }
461
+ async function loadHandlerCtor(match, payload, core, report) {
462
+ const routeId = unhandledRouteId(match);
463
+ let exported;
464
+ try {
465
+ exported = await match.load();
466
+ } catch (caught) {
467
+ logger().error(`Route ${_seedcord_logger.paint.sky.bold(routeId)} failed to load its handler.`, caught);
468
+ await answer(caught, freshScope(match, payload, core), report, "failed");
469
+ return null;
470
+ }
471
+ if (isHandlerCtor(exported)) return exported;
472
+ logger().error(`Route ${_seedcord_logger.paint.sky.bold(routeId)} loaded an export that is not a handler class.`);
473
+ await answer(/* @__PURE__ */ new Error(`route ${routeId} loaded an export that is not a handler class`), freshScope(match, payload, core), report, "failed");
474
+ return null;
475
+ }
323
476
  /**
324
- * Runs the pre-ack phase for a matched interaction. Loads the route module, constructs the handler, and
477
+ * Runs the pre-ack phase for a matched interaction. Loads the route's handler class, constructs it, and
325
478
  * runs its gates, and the sender posts any refusal. Returns the post-ack execute continuation, or null
326
479
  * when a gate refuses or nothing can run.
327
480
  */
328
481
  async function dispatchInteraction(args) {
329
482
  const { match, payload, core } = args;
330
- let moduleExports;
331
- try {
332
- moduleExports = await match.load();
333
- } catch (caught) {
334
- logger().error(`Route ${_seedcord_logger.paint.sky.bold(match.routeId ?? "unhandled")} failed to load its module.`, caught);
335
- await handleFault(caught, freshScope(match, payload, core));
336
- return null;
337
- }
338
- const ctor = handlerCtorOf(moduleExports);
339
- if (!ctor) {
340
- const routeId = match.routeId ?? "unhandled";
341
- logger().error(`Route ${_seedcord_logger.paint.sky.bold(routeId)} loaded a module with no handler class.`);
342
- await handleFault(/* @__PURE__ */ new Error(`route ${routeId} loaded a module with no handler class`), freshScope(match, payload, core));
343
- return null;
344
- }
345
- const dispatch = new _seedcord_core.DispatchContext(match.routeId);
483
+ const report = dispatchReporter(match, payload, core);
484
+ const ctor = await loadHandlerCtor(match, payload, core, report);
485
+ if (!ctor) return null;
486
+ const dispatch = new _seedcord_core.DispatchContext(unhandledRouteId(match));
346
487
  let handler;
347
488
  try {
348
489
  handler = new ctor(payload, core, dispatch);
349
490
  } catch (caught) {
350
- await handleFault(caught, freshScope(match, payload, core));
491
+ await answer(caught, freshScope(match, payload, core), report);
351
492
  return null;
352
493
  }
353
494
  const scope = {
354
495
  core,
355
496
  payload,
356
- routeId: match.routeId ?? handler.constructor.name,
497
+ routeId: unhandledRouteId(match),
357
498
  sender: handler instanceof RepliableHandler ? handler.getSender() : null
358
499
  };
359
- if (!await passedGates(ctor, match, payload, core, scope)) return null;
500
+ const refusal = await gateRefusal(ctor, match, payload, core);
501
+ if (refusal) {
502
+ await answer(refusal.caught, scope, report);
503
+ return null;
504
+ }
360
505
  return async () => {
361
506
  try {
362
507
  await handler.execute();
508
+ report("handled");
363
509
  } catch (caught) {
364
- await handleFault(caught, scope);
510
+ await answer(caught, scope, report);
365
511
  }
366
512
  };
367
513
  }
368
- async function passedGates(ctor, match, payload, core, scope) {
369
- if (match.kind === "autocomplete" || payload.type === discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete) return true;
514
+ async function gateRefusal(ctor, match, payload, core) {
515
+ if (match.kind === "autocomplete" || payload.type === discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete) return null;
370
516
  const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
371
517
  try {
372
518
  await (0, _seedcord_core_internal.runHandlerGates)(ctor, interactionGateContext(payload, core, match.routeId), match.routeId ?? void 0, monitor?.observe);
373
- return true;
519
+ return null;
374
520
  } catch (caught) {
375
- await handleFault(caught, scope);
376
- return false;
521
+ return { caught };
377
522
  } finally {
378
523
  monitor?.report(match.routeId);
379
524
  }
380
525
  }
381
526
 
527
+ //#endregion
528
+ //#region src/dispatch/registerSubscribers.ts
529
+ /**
530
+ * Registers a manifest's subscribers on the bus without importing them. Each row's module loads on
531
+ * the first publish of one of its keys, which keeps an isolate that never publishes free of the cost.
532
+ *
533
+ * @internal
534
+ */
535
+ function registerSubscribers(bus, manifest) {
536
+ for (const row of manifest.subscriberRoutes) bus.register({
537
+ keys: row.keys,
538
+ frequency: row.frequency,
539
+ resolve: async () => {
540
+ const keys = row.keys.join(", ");
541
+ let module;
542
+ try {
543
+ module = await row.load();
544
+ } catch (caught) {
545
+ throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.RouteModuleLoadFailed, [keys, row.from], { cause: caught });
546
+ }
547
+ if (!Object.hasOwn(module, row.exportName)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionRouteExportMissing, [
548
+ keys,
549
+ row.exportName,
550
+ row.from
551
+ ]);
552
+ const exported = module[row.exportName];
553
+ if (typeof exported !== "function" || !(exported.prototype instanceof _seedcord_core.Subscriber)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.SubscriberRouteNotASubscriber, [
554
+ keys,
555
+ row.exportName,
556
+ row.from
557
+ ]);
558
+ return exported;
559
+ }
560
+ });
561
+ }
562
+
382
563
  //#endregion
383
564
  //#region src/inputs/HttpSlashOptions.ts
384
565
  var HttpSlashOptions = class {
@@ -455,20 +636,6 @@ function numeric(value) {
455
636
  return Number.isNaN(parsed) ? null : parsed;
456
637
  }
457
638
 
458
- //#endregion
459
- //#region src/dispatch/slashRouteOf.ts
460
- /**
461
- * Computes the dispatch route for a chat-input or autocomplete payload, the raw-payload port of the
462
- * gateway's `slashRouteOf`. Both assemble keys through `buildSlashRoute`, so a dispatched interaction
463
- * and a generated registry key always match.
464
- */
465
- function slashRouteOf(data) {
466
- const first = data.options?.[0];
467
- if (first?.type === discord_api_types_v10.ApplicationCommandOptionType.SubcommandGroup) return (0, _seedcord_utils_internal.buildSlashRoute)(data.name, first.options[0]?.name, first.name);
468
- if (first?.type === discord_api_types_v10.ApplicationCommandOptionType.Subcommand) return (0, _seedcord_utils_internal.buildSlashRoute)(data.name, first.name);
469
- return data.name;
470
- }
471
-
472
639
  //#endregion
473
640
  //#region src/handlers/interaction/AutocompleteHandler.ts
474
641
  /**
@@ -482,7 +649,7 @@ function slashRouteOf(data) {
482
649
  */
483
650
  var AutocompleteHandler = class extends _seedcord_core.BaseHandler {
484
651
  constructor(event, core, dispatch) {
485
- super(event, core, dispatch);
652
+ super(event, core, dispatch, "interactions");
486
653
  }
487
654
  /**
488
655
  * Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
@@ -525,10 +692,14 @@ var AutocompleteHandler = class extends _seedcord_core.BaseHandler {
525
692
  }
526
693
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
527
694
  async respond(choices) {
528
- await this.core.rest.post(discord_api_types_v10.Routes.interactionCallback(this.event.id, this.event.token), { body: {
695
+ const routeId = this.dispatch?.routeId ?? "autocomplete";
696
+ await (0, _seedcord_core_internal.reportedWrite)({
697
+ bus: this.core.bus,
698
+ interactionId: this.event.id
699
+ }, routeId, "respond", () => this.core.rest.post(discord_api_types_v10.Routes.interactionCallback(this.event.id, this.event.token), { body: {
529
700
  type: discord_api_types_v10.InteractionResponseType.ApplicationCommandAutocompleteResult,
530
701
  data: { choices }
531
- } });
702
+ } }));
532
703
  }
533
704
  /** The firing command route, for a field whose completion differs per registered command. */
534
705
  get route() {
@@ -626,14 +797,30 @@ function componentMapKey(type) {
626
797
  default: return null;
627
798
  }
628
799
  }
629
- function set(map, kind, key, load) {
630
- map.set(key, {
631
- kind,
632
- routeId: `${kind}:${key}`,
633
- load
634
- });
800
+ function namedExport(routeId, row) {
801
+ return async () => {
802
+ let moduleExports;
803
+ try {
804
+ moduleExports = await row.load();
805
+ } catch (caught) {
806
+ throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.RouteModuleLoadFailed, [routeId, row.from], { cause: caught });
807
+ }
808
+ if (!Object.hasOwn(moduleExports, row.exportName)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionRouteExportMissing, [
809
+ routeId,
810
+ row.exportName,
811
+ row.from
812
+ ]);
813
+ return moduleExports[row.exportName];
814
+ };
635
815
  }
636
- /** Builds the per-kind lookup maps. */
816
+ function describeRow(row) {
817
+ return `${row.exportName} (${row.from})`;
818
+ }
819
+ /**
820
+ * Builds the per-kind lookup maps.
821
+ *
822
+ * @throws A **SeedcordError** when two rows resolve to the same route.
823
+ */
637
824
  function buildRouteMaps(manifest) {
638
825
  const maps = {
639
826
  slash: /* @__PURE__ */ new Map(),
@@ -650,19 +837,36 @@ function buildRouteMaps(manifest) {
650
837
  modal: /* @__PURE__ */ new Map()
651
838
  }
652
839
  };
653
- for (const row of manifest.commands) {
840
+ const owners = /* @__PURE__ */ new Map();
841
+ function set(map, kind, key, row) {
842
+ const routeId = `${kind}:${key}`;
843
+ const owner = owners.get(routeId);
844
+ if (owner) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
845
+ routeId,
846
+ describeRow(owner),
847
+ describeRow(row)
848
+ ]);
849
+ owners.set(routeId, row);
850
+ map.set(key, {
851
+ kind,
852
+ routeId,
853
+ load: namedExport(routeId, row)
854
+ });
855
+ }
856
+ for (const row of manifest.commandRoutes) {
654
857
  const kind = commandKind(row.type);
655
- if (kind) set(maps[kind], kind, row.name, row.load);
858
+ if (kind) set(maps[kind], kind, row.name, row);
656
859
  }
657
- for (const row of manifest.autocomplete) set(maps.autocomplete, "autocomplete", row.name, row.load);
658
- for (const row of manifest.components) set(maps.components[row.kind], COMPONENT_KIND[row.kind], row.prefix, row.load);
860
+ for (const row of manifest.autocompleteRoutes) set(maps.autocomplete, "autocomplete", row.name, row);
861
+ for (const row of manifest.componentRoutes) set(maps.components[row.kind], COMPONENT_KIND[row.kind], row.prefix, row);
659
862
  return maps;
660
863
  }
661
- function unhandled(kind) {
864
+ function unhandled(kind, attemptedKey) {
662
865
  return {
663
866
  kind,
664
867
  routeId: null,
665
- load: () => Promise.resolve(kind === "autocomplete" ? { UnhandledAutocomplete } : { UnhandledRepliable })
868
+ attemptedKey,
869
+ load: () => Promise.resolve(kind === "autocomplete" ? UnhandledAutocomplete : UnhandledRepliable)
666
870
  };
667
871
  }
668
872
  /**
@@ -675,17 +879,27 @@ function unhandled(kind) {
675
879
  function resolve(maps, interaction) {
676
880
  switch (interaction.type) {
677
881
  case discord_api_types_v10.InteractionType.ApplicationCommand: {
678
- if (interaction.data.type === discord_api_types_v10.ApplicationCommandType.ChatInput) return maps.slash.get(slashRouteOf(interaction.data)) ?? unhandled("slash");
882
+ if (interaction.data.type === discord_api_types_v10.ApplicationCommandType.ChatInput) {
883
+ const route = slashRouteOf(interaction.data);
884
+ return maps.slash.get(route) ?? unhandled("slash", route);
885
+ }
679
886
  const kind = commandKind(interaction.data.type);
680
- return kind ? maps[kind].get(interaction.data.name) ?? unhandled(kind) : null;
887
+ return kind ? maps[kind].get(interaction.data.name) ?? unhandled(kind, interaction.data.name) : null;
888
+ }
889
+ case discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete: {
890
+ const route = slashRouteOf(interaction.data);
891
+ return maps.autocomplete.get(route) ?? unhandled("autocomplete", route);
681
892
  }
682
- case discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete: return maps.autocomplete.get(slashRouteOf(interaction.data)) ?? unhandled("autocomplete");
683
893
  case discord_api_types_v10.InteractionType.MessageComponent: {
684
894
  const key = componentMapKey(interaction.data.component_type);
685
895
  if (!key) return null;
686
- return maps.components[key].get((0, _seedcord_core_internal.prefixOf)(interaction.data.custom_id)) ?? unhandled(COMPONENT_KIND[key]);
896
+ const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.data.custom_id);
897
+ return maps.components[key].get(prefix) ?? unhandled(COMPONENT_KIND[key], prefix);
898
+ }
899
+ case discord_api_types_v10.InteractionType.ModalSubmit: {
900
+ const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.data.custom_id);
901
+ return maps.components.modal.get(prefix) ?? unhandled("modal", prefix);
687
902
  }
688
- case discord_api_types_v10.InteractionType.ModalSubmit: return maps.components.modal.get((0, _seedcord_core_internal.prefixOf)(interaction.data.custom_id)) ?? unhandled("modal");
689
903
  default: return null;
690
904
  }
691
905
  }
@@ -766,7 +980,7 @@ function buildEngine(core, maps) {
766
980
  const verifier = new Ed25519Verifier(envapt.Envapter.getRequired("DISCORD_PUBLIC_KEY", envapt.Converters.String));
767
981
  const replays = new ReplayGuard();
768
982
  const inFlight = /* @__PURE__ */ new Set();
769
- const logger = new _seedcord_logger.Logger("Engine");
983
+ const logger = new _seedcord_logger.Logger("Engine", { channel: "bot" });
770
984
  async function dispatchMatched(match, payload, ctx) {
771
985
  const start = await dispatchInteraction({
772
986
  match,
@@ -774,13 +988,18 @@ function buildEngine(core, maps) {
774
988
  core
775
989
  });
776
990
  if (!start) return;
777
- const work = start();
991
+ const work = start().catch(rootFault);
778
992
  if (ctx) ctx.waitUntil(work);
779
993
  else {
780
994
  inFlight.add(work);
781
995
  work.finally(() => inFlight.delete(work));
782
996
  }
783
997
  }
998
+ function rootFault(caught) {
999
+ const error = (0, _seedcord_core_internal.asError)(caught);
1000
+ logger.error("dispatch failed past the boundary", error);
1001
+ core.bus[_seedcord_core_internal.PublishDefault]("unhandledInteractionError", { error });
1002
+ }
784
1003
  const handle = async (request, ctx) => {
785
1004
  if (request.method !== "POST") return new Response(null, {
786
1005
  status: METHOD_NOT_ALLOWED,
@@ -801,10 +1020,11 @@ function buildEngine(core, maps) {
801
1020
  if (!hasInteractionType(payload)) return new Response(null, { status: BAD_REQUEST });
802
1021
  if (payload.type === PING) return Response.json({ type: discord_api_types_v10.InteractionResponseType.Pong });
803
1022
  try {
1023
+ core.bus[_seedcord_core_internal.PublishDefault]("anyInteraction", { interaction: payload });
804
1024
  const match = resolve(maps, payload);
805
1025
  if (match) await dispatchMatched(match, payload, ctx);
806
1026
  } catch (caught) {
807
- logger.error("dispatch failed, acking anyway", caught);
1027
+ rootFault(caught);
808
1028
  }
809
1029
  return new Response(null, { status: ACCEPTED });
810
1030
  };
@@ -830,7 +1050,10 @@ function buildEngine(core, maps) {
830
1050
  * `SeedcordError` when either is missing or malformed.
831
1051
  */
832
1052
  function createSeedcord(config, manifest) {
833
- return buildEngine(createCore(config, (0, _seedcord_errors_internal.validateDiscordToken)(envapt.Envapter.get("DISCORD_BOT_TOKEN"))), buildRouteMaps(manifest)).handle;
1053
+ const core = createCore(config, (0, _seedcord_errors_internal.validateDiscordToken)(envapt.Envapter.get("DISCORD_BOT_TOKEN")));
1054
+ core.bus.registerDefaults();
1055
+ registerSubscribers(core.bus, manifest);
1056
+ return buildEngine(core, buildRouteMaps(manifest)).handle;
834
1057
  }
835
1058
 
836
1059
  //#endregion
@@ -1029,6 +1252,22 @@ var ModalHandler = class extends ComponentHandler {
1029
1252
  }
1030
1253
  };
1031
1254
 
1255
+ //#endregion
1256
+ //#region src/subscribers/index.ts
1257
+ /**
1258
+ * Base class for an HTTP subscriber, binding `this.core` to the HTTP {@link Core}.
1259
+ *
1260
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
1261
+ */
1262
+ var Subscriber = class extends _seedcord_core.Subscriber {};
1263
+ /**
1264
+ * Base class for an HTTP subscriber that delivers its event to a Discord webhook, binding
1265
+ * `this.core` to the HTTP {@link Core}.
1266
+ *
1267
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1268
+ */
1269
+ var WebhookLog = class extends _seedcord_core.WebhookLog {};
1270
+
1032
1271
  //#endregion
1033
1272
  //#region src/gates/Gated.ts
1034
1273
  /**
@@ -1073,7 +1312,7 @@ function Gated(...gates) {
1073
1312
  //#endregion
1074
1313
  //#region src/version.ts
1075
1314
  /** Package version */
1076
- const version = "0.1.0-next.1";
1315
+ const version = "0.1.0-next.2";
1077
1316
 
1078
1317
  //#endregion
1079
1318
  Object.defineProperty(exports, 'AutocompleteHandler', {
@@ -1136,6 +1375,18 @@ Object.defineProperty(exports, 'SlashHandler', {
1136
1375
  return SlashHandler;
1137
1376
  }
1138
1377
  });
1378
+ Object.defineProperty(exports, 'Subscriber', {
1379
+ enumerable: true,
1380
+ get: function () {
1381
+ return Subscriber;
1382
+ }
1383
+ });
1384
+ Object.defineProperty(exports, 'WebhookLog', {
1385
+ enumerable: true,
1386
+ get: function () {
1387
+ return WebhookLog;
1388
+ }
1389
+ });
1139
1390
  Object.defineProperty(exports, '__exportAll', {
1140
1391
  enumerable: true,
1141
1392
  get: function () {
@@ -1172,4 +1423,4 @@ Object.defineProperty(exports, 'version', {
1172
1423
  return version;
1173
1424
  }
1174
1425
  });
1175
- //# sourceMappingURL=edge.index-D-60VfMn.cjs.map
1426
+ //# sourceMappingURL=edge.index-joDL39cK.cjs.map