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

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.
@@ -1,7 +1,9 @@
1
1
  //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __exportAll = (all, no_symbols) => {
7
9
  let target = {};
@@ -31,6 +33,10 @@ var __copyProps = (to, from, except, desc) => {
31
33
  return to;
32
34
  };
33
35
  var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
36
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
37
+ value: mod,
38
+ enumerable: true
39
+ }) : target, mod));
34
40
 
35
41
  //#endregion
36
42
  let _seedcord_errors_internal = require("@seedcord/errors/internal");
@@ -42,8 +48,8 @@ let _seedcord_logger = require("@seedcord/logger");
42
48
  let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
43
49
  let discord_api_types_v10 = require("discord-api-types/v10");
44
50
  let _discordjs_core_http_only = require("@discordjs/core/http-only");
45
- let _seedcord_errors = require("@seedcord/errors");
46
51
  let _seedcord_utils_internal = require("@seedcord/utils/internal");
52
+ let _seedcord_errors = require("@seedcord/errors");
47
53
 
48
54
  //#region src/reply/ReplySender.ts
49
55
  function wireAllowedMentions(mentions) {
@@ -77,8 +83,11 @@ function callbackData(reply, flags) {
77
83
  var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
78
84
  ref;
79
85
  rest;
80
- constructor(ref, rest, routeId) {
81
- super(routeId);
86
+ constructor(ref, rest, routeId, bus) {
87
+ super(routeId, "unacked", bus && {
88
+ bus,
89
+ interactionId: ref.id
90
+ });
82
91
  this.ref = ref;
83
92
  this.rest = rest;
84
93
  }
@@ -174,12 +183,16 @@ function apiFor(rest) {
174
183
  * @typeParam Event - The repliable interaction type this handler processes
175
184
  */
176
185
  var RepliableHandler = class extends _seedcord_core.RepliableHandler {
186
+ /** @internal */
187
+ getEvent() {
188
+ return this.event;
189
+ }
177
190
  buildSender(event, core, routeId) {
178
191
  return new ReplySender({
179
192
  application_id: event.application_id,
180
193
  id: event.id,
181
194
  token: event.token
182
- }, core.rest, routeId);
195
+ }, core.rest, routeId, core.bus);
183
196
  }
184
197
  /**
185
198
  * Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
@@ -212,31 +225,132 @@ function interactionGateContext(payload, core, routeId) {
212
225
  };
213
226
  }
214
227
 
228
+ //#endregion
229
+ //#region src/dispatch/slashRouteOf.ts
230
+ /**
231
+ * Computes the dispatch route for a chat-input or autocomplete payload, the raw-payload port of the
232
+ * gateway's `slashRouteOf`. Both assemble keys through `buildSlashRoute`, so a dispatched interaction
233
+ * and a generated registry key always match.
234
+ */
235
+ function slashRouteOf(data) {
236
+ const first = data.options?.[0];
237
+ if (first?.type === discord_api_types_v10.ApplicationCommandOptionType.SubcommandGroup) return (0, _seedcord_utils_internal.buildSlashRoute)(data.name, first.options[0]?.name, first.name);
238
+ if (first?.type === discord_api_types_v10.ApplicationCommandOptionType.Subcommand) return (0, _seedcord_utils_internal.buildSlashRoute)(data.name, first.name);
239
+ return data.name;
240
+ }
241
+
242
+ //#endregion
243
+ //#region src/dispatch/reportFault.ts
244
+ let faultLogger;
245
+ function logger$1() {
246
+ faultLogger ??= new _seedcord_logger.Logger("Faults", { channel: "errors" });
247
+ return faultLogger;
248
+ }
249
+ /**
250
+ * Publishes a fault on the bus, the raw-payload equivalent of the reporting half of gateway's
251
+ * `extractErrorResponse`. A reported {@link Notice} publishes `handledException`, everything else
252
+ * publishes `unknownException`.
253
+ *
254
+ * @internal
255
+ */
256
+ function reportFault(error, uuid, payload, core) {
257
+ const throttle = _seedcord_core_internal.FaultThrottle.for(core);
258
+ const key = `${routeKeyOf(payload)}:${nameOf(error)}`;
259
+ if (!throttle.shouldReport(key)) {
260
+ logger$1().debug(`throttled duplicate fault ${_seedcord_logger.paint.mute(key)}`);
261
+ return;
262
+ }
263
+ const source = interactionSource(payload);
264
+ if (error instanceof _seedcord_core.Notice && source) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
265
+ denial: error,
266
+ uuid,
267
+ source
268
+ });
269
+ else core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
270
+ uuid,
271
+ error,
272
+ ...actors(payload),
273
+ metadata: payload
274
+ });
275
+ throttle.markReported(key);
276
+ }
277
+ function routeKeyOf(payload) {
278
+ if (payload.type === discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete) return `autocomplete:${slashRouteOf(payload.data)}`;
279
+ 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;
280
+ return (0, _seedcord_core_internal.prefixOf)(payload.data.custom_id) || payload.data.custom_id;
281
+ }
282
+ function nameOf(error) {
283
+ if (error instanceof _seedcord_core.Notice) return error.name;
284
+ if (error instanceof _discordjs_rest.DiscordAPIError) return String(error.code);
285
+ return error.constructor.name;
286
+ }
287
+ function actors(payload) {
288
+ const user = payload.member?.user ?? payload.user;
289
+ return {
290
+ guild: void 0,
291
+ user: user ? {
292
+ id: user.id,
293
+ username: user.username
294
+ } : void 0
295
+ };
296
+ }
297
+ function interactionSource(payload) {
298
+ const user = payload.member?.user ?? payload.user;
299
+ if (!user) return null;
300
+ const common = {
301
+ kind: "interaction",
302
+ userId: user.id,
303
+ guildId: payload.guild_id ?? null,
304
+ channelId: payload.channel?.id ?? null,
305
+ interactionId: payload.id,
306
+ raw: payload
307
+ };
308
+ if (payload.type === discord_api_types_v10.InteractionType.ApplicationCommand) {
309
+ const isSlash = payload.data.type === discord_api_types_v10.ApplicationCommandType.ChatInput;
310
+ return {
311
+ ...common,
312
+ interactionKind: isSlash ? "slash" : "context-menu",
313
+ command: routeKeyOf(payload),
314
+ customId: null
315
+ };
316
+ }
317
+ if (payload.type === discord_api_types_v10.InteractionType.ModalSubmit) return {
318
+ ...common,
319
+ interactionKind: "modal",
320
+ command: null,
321
+ customId: payload.data.custom_id
322
+ };
323
+ if (payload.type === discord_api_types_v10.InteractionType.MessageComponent) {
324
+ const isButton = payload.data.component_type === discord_api_types_v10.ComponentType.Button;
325
+ return {
326
+ ...common,
327
+ interactionKind: isButton ? "button" : "select",
328
+ command: null,
329
+ customId: payload.data.custom_id
330
+ };
331
+ }
332
+ return null;
333
+ }
334
+
215
335
  //#endregion
216
336
  //#region src/dispatch/dispatchInteraction.ts
217
337
  let dispatchLogger;
218
338
  function logger() {
219
- dispatchLogger ??= new _seedcord_logger.Logger("Dispatcher");
339
+ dispatchLogger ??= new _seedcord_logger.Logger("Dispatcher", { channel: "interactions" });
220
340
  return dispatchLogger;
221
341
  }
222
342
  function createCore(config, token) {
223
- return {
343
+ const draft = {
224
344
  config,
225
345
  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"
346
+ rest: new _discordjs_rest.REST().setToken(token)
230
347
  };
348
+ draft.bus = new _seedcord_core.Bus(draft);
349
+ return draft;
231
350
  }
232
351
  function isHandlerCtor(value) {
233
352
  return typeof value === "function" && value.prototype instanceof _seedcord_core.BaseHandler;
234
353
  }
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
354
  const HARMLESS_API_CODES = new Set([
241
355
  discord_api_types_v10.RESTJSONErrorCodes.UnknownInteraction,
242
356
  discord_api_types_v10.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
@@ -254,10 +368,13 @@ async function sendGuarded(routeId, send) {
254
368
  }
255
369
  }
256
370
  async function respondEmptyChoices(scope) {
257
- await scope.core.rest.post(discord_api_types_v10.Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
371
+ await (0, _seedcord_core_internal.reportedWrite)({
372
+ bus: scope.core.bus,
373
+ interactionId: scope.payload.id
374
+ }, scope.routeId, "respond", () => scope.core.rest.post(discord_api_types_v10.Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
258
375
  type: discord_api_types_v10.InteractionResponseType.ApplicationCommandAutocompleteResult,
259
376
  data: { choices: [] }
260
- } });
377
+ } }));
261
378
  }
262
379
  function renderContext(core, uuid) {
263
380
  const developerUsername = core.config.notifications?.developerUsername;
@@ -267,7 +384,10 @@ function renderContext(core, uuid) {
267
384
  };
268
385
  }
269
386
  async function handleNotice(notice, uuid, scope) {
270
- if (notice.report) logger().error(`${notice.name}: ${_seedcord_logger.paint.mute(uuid)}`, notice);
387
+ if (notice.report) {
388
+ logger().error(`${notice.name}: ${_seedcord_logger.paint.mute(uuid)}`, notice);
389
+ reportFault(notice, uuid, scope.payload, scope.core);
390
+ }
271
391
  const { sender } = scope;
272
392
  if (!sender) {
273
393
  await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
@@ -280,6 +400,7 @@ async function handleRawFault(error, uuid, scope) {
280
400
  const { core, sender } = scope;
281
401
  if (core.config.errors?.errorStack ?? false) logger().error(_seedcord_logger.paint.mute(uuid), error);
282
402
  else logger().error(`${_seedcord_logger.paint.mute(uuid)} | ${error.message}`);
403
+ reportFault(error, uuid, scope.payload, core);
283
404
  if (!sender) {
284
405
  await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
285
406
  return;
@@ -290,7 +411,7 @@ async function handleRawFault(error, uuid, scope) {
290
411
  }
291
412
  async function handleFault(caught, scope) {
292
413
  if (caught instanceof _seedcord_core.Silence) {
293
- if (caught.reason !== void 0) logger().debug(`Silence: ${caught.reason}`);
414
+ if (caught.reason !== void 0 && (scope.core.config.errors?.logSilences ?? true)) logger().debug(`Silence: ${caught.reason}`);
294
415
  return;
295
416
  }
296
417
  const uuid = crypto.randomUUID();
@@ -298,7 +419,7 @@ async function handleFault(caught, scope) {
298
419
  await handleNotice(caught, uuid, scope);
299
420
  return;
300
421
  }
301
- const error = Error.isError(caught) ? caught : new Error(String(caught));
422
+ const error = (0, _seedcord_core_internal.asError)(caught);
302
423
  const ignore = new Set(scope.core.config.errors?.ignoreApiCodes ?? []);
303
424
  if (error instanceof _discordjs_rest.DiscordAPIError && ignore.has(error.code)) {
304
425
  logger().debug(`swallowed api code ${_seedcord_logger.paint.amber(String(error.code))}`);
@@ -306,79 +427,149 @@ async function handleFault(caught, scope) {
306
427
  }
307
428
  await handleRawFault(error, uuid, scope);
308
429
  }
430
+ function unhandledRouteId(match) {
431
+ if (match.routeId) return match.routeId;
432
+ const key = match.attemptedKey ?? "";
433
+ return `${match.kind}:${key.length > 0 ? key : "unrouted"}`;
434
+ }
309
435
  function freshScope(match, payload, core) {
310
436
  const ref = {
311
437
  application_id: payload.application_id,
312
438
  id: payload.id,
313
439
  token: payload.token
314
440
  };
315
- const routeId = match.routeId ?? "unhandled";
441
+ const routeId = unhandledRouteId(match);
316
442
  return {
317
443
  core,
318
444
  payload,
319
445
  routeId,
320
- sender: match.kind === "autocomplete" ? null : new ReplySender(ref, core.rest, routeId)
446
+ sender: match.kind === "autocomplete" ? null : new ReplySender(ref, core.rest, routeId, core.bus)
321
447
  };
322
448
  }
323
- /**
324
- * Runs the pre-ack phase for a matched interaction. Loads the route module, constructs the handler, and
325
- * runs its gates, and the sender posts any refusal. Returns the post-ack execute continuation, or null
326
- * when a gate refuses or nothing can run.
327
- */
328
- async function dispatchInteraction(args) {
329
- const { match, payload, core } = args;
330
- let moduleExports;
449
+ function dispatchReporter(match, payload, core) {
450
+ const startedAt = performance.now();
451
+ const queuedMs = (0, _seedcord_core_internal.queuedMsFor)(payload.id);
452
+ return (outcome) => {
453
+ core.bus[_seedcord_core_internal.PublishDefault]("interactionDispatched", (0, _seedcord_core_internal.dispatchedPayload)({
454
+ routeId: unhandledRouteId(match),
455
+ interactionId: payload.id,
456
+ kind: match.kind,
457
+ outcome,
458
+ fallback: match.routeId === null,
459
+ startedAt,
460
+ queuedMs
461
+ }));
462
+ };
463
+ }
464
+ async function answer(caught, scope, report, outcome = (0, _seedcord_core_internal.outcomeFor)(caught)) {
331
465
  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;
466
+ await handleFault(caught, scope);
467
+ } finally {
468
+ report(outcome);
337
469
  }
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));
470
+ }
471
+ async function loadHandlerCtor(match, payload, core, report) {
472
+ const routeId = unhandledRouteId(match);
473
+ let exported;
474
+ try {
475
+ exported = await match.load();
476
+ } catch (caught) {
477
+ logger().error(`Route ${_seedcord_logger.paint.sky.bold(routeId)} failed to load its handler.`, caught);
478
+ await answer(caught, freshScope(match, payload, core), report, "failed");
343
479
  return null;
344
480
  }
345
- const dispatch = new _seedcord_core.DispatchContext(match.routeId);
481
+ if (isHandlerCtor(exported)) return exported;
482
+ logger().error(`Route ${_seedcord_logger.paint.sky.bold(routeId)} loaded an export that is not a handler class.`);
483
+ await answer(/* @__PURE__ */ new Error(`route ${routeId} loaded an export that is not a handler class`), freshScope(match, payload, core), report, "failed");
484
+ return null;
485
+ }
486
+ /**
487
+ * Runs the pre-ack phase for a matched interaction. Loads the route's handler class, constructs it, and
488
+ * runs its gates. The sender posts any refusal. Returns the post-ack execute continuation, or null when a
489
+ * gate refuses or nothing can run.
490
+ */
491
+ async function dispatchInteraction(args) {
492
+ const { match, payload, core } = args;
493
+ const report = dispatchReporter(match, payload, core);
494
+ const ctor = await loadHandlerCtor(match, payload, core, report);
495
+ if (!ctor) return null;
496
+ const dispatch = new _seedcord_core.DispatchContext(unhandledRouteId(match));
346
497
  let handler;
347
498
  try {
348
499
  handler = new ctor(payload, core, dispatch);
349
500
  } catch (caught) {
350
- await handleFault(caught, freshScope(match, payload, core));
501
+ await answer(caught, freshScope(match, payload, core), report);
351
502
  return null;
352
503
  }
353
504
  const scope = {
354
505
  core,
355
506
  payload,
356
- routeId: match.routeId ?? handler.constructor.name,
507
+ routeId: unhandledRouteId(match),
357
508
  sender: handler instanceof RepliableHandler ? handler.getSender() : null
358
509
  };
359
- if (!await passedGates(ctor, match, payload, core, scope)) return null;
510
+ const refusal = await gateRefusal(ctor, match, payload, core);
511
+ if (refusal) {
512
+ await answer(refusal.caught, scope, report);
513
+ return null;
514
+ }
360
515
  return async () => {
361
516
  try {
362
517
  await handler.execute();
518
+ report("handled");
363
519
  } catch (caught) {
364
- await handleFault(caught, scope);
520
+ await answer(caught, scope, report);
365
521
  }
366
522
  };
367
523
  }
368
- async function passedGates(ctor, match, payload, core, scope) {
369
- if (match.kind === "autocomplete" || payload.type === discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete) return true;
524
+ async function gateRefusal(ctor, match, payload, core) {
525
+ if (match.kind === "autocomplete" || payload.type === discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete) return null;
370
526
  const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
371
527
  try {
372
528
  await (0, _seedcord_core_internal.runHandlerGates)(ctor, interactionGateContext(payload, core, match.routeId), match.routeId ?? void 0, monitor?.observe);
373
- return true;
529
+ return null;
374
530
  } catch (caught) {
375
- await handleFault(caught, scope);
376
- return false;
531
+ return { caught };
377
532
  } finally {
378
533
  monitor?.report(match.routeId);
379
534
  }
380
535
  }
381
536
 
537
+ //#endregion
538
+ //#region src/dispatch/registerSubscribers.ts
539
+ /**
540
+ * Registers a manifest's subscribers on the bus without importing them. Each row's module loads on
541
+ * the first publish of one of its keys, which keeps an isolate that never publishes free of the cost.
542
+ *
543
+ * @internal
544
+ */
545
+ function registerSubscribers(bus, manifest) {
546
+ for (const row of manifest.subscriberRoutes) bus.register({
547
+ keys: row.keys,
548
+ frequency: row.frequency,
549
+ resolve: async () => {
550
+ const keys = row.keys.join(", ");
551
+ let module;
552
+ try {
553
+ module = await row.load();
554
+ } catch (caught) {
555
+ throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.RouteModuleLoadFailed, [keys, row.from], { cause: caught });
556
+ }
557
+ if (!Object.hasOwn(module, row.exportName)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionRouteExportMissing, [
558
+ keys,
559
+ row.exportName,
560
+ row.from
561
+ ]);
562
+ const exported = module[row.exportName];
563
+ if (typeof exported !== "function" || !(exported.prototype instanceof _seedcord_core.Subscriber)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.SubscriberRouteNotASubscriber, [
564
+ keys,
565
+ row.exportName,
566
+ row.from
567
+ ]);
568
+ return exported;
569
+ }
570
+ });
571
+ }
572
+
382
573
  //#endregion
383
574
  //#region src/inputs/HttpSlashOptions.ts
384
575
  var HttpSlashOptions = class {
@@ -455,20 +646,6 @@ function numeric(value) {
455
646
  return Number.isNaN(parsed) ? null : parsed;
456
647
  }
457
648
 
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
649
  //#endregion
473
650
  //#region src/handlers/interaction/AutocompleteHandler.ts
474
651
  /**
@@ -482,7 +659,7 @@ function slashRouteOf(data) {
482
659
  */
483
660
  var AutocompleteHandler = class extends _seedcord_core.BaseHandler {
484
661
  constructor(event, core, dispatch) {
485
- super(event, core, dispatch);
662
+ super(event, core, dispatch, "interactions");
486
663
  }
487
664
  /**
488
665
  * Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
@@ -525,10 +702,14 @@ var AutocompleteHandler = class extends _seedcord_core.BaseHandler {
525
702
  }
526
703
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
527
704
  async respond(choices) {
528
- await this.core.rest.post(discord_api_types_v10.Routes.interactionCallback(this.event.id, this.event.token), { body: {
705
+ const routeId = this.dispatch?.routeId ?? "autocomplete";
706
+ await (0, _seedcord_core_internal.reportedWrite)({
707
+ bus: this.core.bus,
708
+ interactionId: this.event.id
709
+ }, routeId, "respond", () => this.core.rest.post(discord_api_types_v10.Routes.interactionCallback(this.event.id, this.event.token), { body: {
529
710
  type: discord_api_types_v10.InteractionResponseType.ApplicationCommandAutocompleteResult,
530
711
  data: { choices }
531
- } });
712
+ } }));
532
713
  }
533
714
  /** The firing command route, for a field whose completion differs per registered command. */
534
715
  get route() {
@@ -626,14 +807,30 @@ function componentMapKey(type) {
626
807
  default: return null;
627
808
  }
628
809
  }
629
- function set(map, kind, key, load) {
630
- map.set(key, {
631
- kind,
632
- routeId: `${kind}:${key}`,
633
- load
634
- });
810
+ function namedExport(routeId, row) {
811
+ return async () => {
812
+ let moduleExports;
813
+ try {
814
+ moduleExports = await row.load();
815
+ } catch (caught) {
816
+ throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.RouteModuleLoadFailed, [routeId, row.from], { cause: caught });
817
+ }
818
+ if (!Object.hasOwn(moduleExports, row.exportName)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionRouteExportMissing, [
819
+ routeId,
820
+ row.exportName,
821
+ row.from
822
+ ]);
823
+ return moduleExports[row.exportName];
824
+ };
825
+ }
826
+ function describeRow(row) {
827
+ return `${row.exportName} (${row.from})`;
635
828
  }
636
- /** Builds the per-kind lookup maps. */
829
+ /**
830
+ * Builds the per-kind lookup maps.
831
+ *
832
+ * @throws A **SeedcordError** when two rows resolve to the same route.
833
+ */
637
834
  function buildRouteMaps(manifest) {
638
835
  const maps = {
639
836
  slash: /* @__PURE__ */ new Map(),
@@ -650,19 +847,36 @@ function buildRouteMaps(manifest) {
650
847
  modal: /* @__PURE__ */ new Map()
651
848
  }
652
849
  };
653
- for (const row of manifest.commands) {
850
+ const owners = /* @__PURE__ */ new Map();
851
+ function set(map, kind, key, row) {
852
+ const routeId = `${kind}:${key}`;
853
+ const owner = owners.get(routeId);
854
+ if (owner) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
855
+ routeId,
856
+ describeRow(owner),
857
+ describeRow(row)
858
+ ]);
859
+ owners.set(routeId, row);
860
+ map.set(key, {
861
+ kind,
862
+ routeId,
863
+ load: namedExport(routeId, row)
864
+ });
865
+ }
866
+ for (const row of manifest.commandRoutes) {
654
867
  const kind = commandKind(row.type);
655
- if (kind) set(maps[kind], kind, row.name, row.load);
868
+ if (kind) set(maps[kind], kind, row.name, row);
656
869
  }
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);
870
+ for (const row of manifest.autocompleteRoutes) set(maps.autocomplete, "autocomplete", row.name, row);
871
+ for (const row of manifest.componentRoutes) set(maps.components[row.kind], COMPONENT_KIND[row.kind], row.prefix, row);
659
872
  return maps;
660
873
  }
661
- function unhandled(kind) {
874
+ function unhandled(kind, attemptedKey) {
662
875
  return {
663
876
  kind,
664
877
  routeId: null,
665
- load: () => Promise.resolve(kind === "autocomplete" ? { UnhandledAutocomplete } : { UnhandledRepliable })
878
+ attemptedKey,
879
+ load: () => Promise.resolve(kind === "autocomplete" ? UnhandledAutocomplete : UnhandledRepliable)
666
880
  };
667
881
  }
668
882
  /**
@@ -675,17 +889,27 @@ function unhandled(kind) {
675
889
  function resolve(maps, interaction) {
676
890
  switch (interaction.type) {
677
891
  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");
892
+ if (interaction.data.type === discord_api_types_v10.ApplicationCommandType.ChatInput) {
893
+ const route = slashRouteOf(interaction.data);
894
+ return maps.slash.get(route) ?? unhandled("slash", route);
895
+ }
679
896
  const kind = commandKind(interaction.data.type);
680
- return kind ? maps[kind].get(interaction.data.name) ?? unhandled(kind) : null;
897
+ return kind ? maps[kind].get(interaction.data.name) ?? unhandled(kind, interaction.data.name) : null;
898
+ }
899
+ case discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete: {
900
+ const route = slashRouteOf(interaction.data);
901
+ return maps.autocomplete.get(route) ?? unhandled("autocomplete", route);
681
902
  }
682
- case discord_api_types_v10.InteractionType.ApplicationCommandAutocomplete: return maps.autocomplete.get(slashRouteOf(interaction.data)) ?? unhandled("autocomplete");
683
903
  case discord_api_types_v10.InteractionType.MessageComponent: {
684
904
  const key = componentMapKey(interaction.data.component_type);
685
905
  if (!key) return null;
686
- return maps.components[key].get((0, _seedcord_core_internal.prefixOf)(interaction.data.custom_id)) ?? unhandled(COMPONENT_KIND[key]);
906
+ const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.data.custom_id);
907
+ return maps.components[key].get(prefix) ?? unhandled(COMPONENT_KIND[key], prefix);
908
+ }
909
+ case discord_api_types_v10.InteractionType.ModalSubmit: {
910
+ const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.data.custom_id);
911
+ return maps.components.modal.get(prefix) ?? unhandled("modal", prefix);
687
912
  }
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
913
  default: return null;
690
914
  }
691
915
  }
@@ -766,7 +990,7 @@ function buildEngine(core, maps) {
766
990
  const verifier = new Ed25519Verifier(envapt.Envapter.getRequired("DISCORD_PUBLIC_KEY", envapt.Converters.String));
767
991
  const replays = new ReplayGuard();
768
992
  const inFlight = /* @__PURE__ */ new Set();
769
- const logger = new _seedcord_logger.Logger("Engine");
993
+ const logger = new _seedcord_logger.Logger("Engine", { channel: "bot" });
770
994
  async function dispatchMatched(match, payload, ctx) {
771
995
  const start = await dispatchInteraction({
772
996
  match,
@@ -774,13 +998,18 @@ function buildEngine(core, maps) {
774
998
  core
775
999
  });
776
1000
  if (!start) return;
777
- const work = start();
1001
+ const work = start().catch(rootFault);
778
1002
  if (ctx) ctx.waitUntil(work);
779
1003
  else {
780
1004
  inFlight.add(work);
781
1005
  work.finally(() => inFlight.delete(work));
782
1006
  }
783
1007
  }
1008
+ function rootFault(caught) {
1009
+ const error = (0, _seedcord_core_internal.asError)(caught);
1010
+ logger.error("dispatch failed past the boundary", error);
1011
+ core.bus[_seedcord_core_internal.PublishDefault]("unhandledInteractionError", { error });
1012
+ }
784
1013
  const handle = async (request, ctx) => {
785
1014
  if (request.method !== "POST") return new Response(null, {
786
1015
  status: METHOD_NOT_ALLOWED,
@@ -801,10 +1030,11 @@ function buildEngine(core, maps) {
801
1030
  if (!hasInteractionType(payload)) return new Response(null, { status: BAD_REQUEST });
802
1031
  if (payload.type === PING) return Response.json({ type: discord_api_types_v10.InteractionResponseType.Pong });
803
1032
  try {
1033
+ core.bus[_seedcord_core_internal.PublishDefault]("anyInteraction", { interaction: payload });
804
1034
  const match = resolve(maps, payload);
805
1035
  if (match) await dispatchMatched(match, payload, ctx);
806
1036
  } catch (caught) {
807
- logger.error("dispatch failed, acking anyway", caught);
1037
+ rootFault(caught);
808
1038
  }
809
1039
  return new Response(null, { status: ACCEPTED });
810
1040
  };
@@ -830,9 +1060,120 @@ function buildEngine(core, maps) {
830
1060
  * `SeedcordError` when either is missing or malformed.
831
1061
  */
832
1062
  function createSeedcord(config, manifest) {
833
- return buildEngine(createCore(config, (0, _seedcord_errors_internal.validateDiscordToken)(envapt.Envapter.get("DISCORD_BOT_TOKEN"))), buildRouteMaps(manifest)).handle;
1063
+ const core = createCore(config, (0, _seedcord_errors_internal.validateDiscordToken)(envapt.Envapter.get("DISCORD_BOT_TOKEN")));
1064
+ core.bus.registerDefaults();
1065
+ registerSubscribers(core.bus, manifest);
1066
+ return buildEngine(core, buildRouteMaps(manifest)).handle;
834
1067
  }
835
1068
 
1069
+ //#endregion
1070
+ //#region src/applicationId.ts
1071
+ /**
1072
+ * Reads the bot's own application id over REST. A gateway host takes it off the logged-in client, and
1073
+ * an http host has no session to read it from.
1074
+ *
1075
+ * @internal
1076
+ */
1077
+ async function fetchApplicationId(rest) {
1078
+ try {
1079
+ return (await rest.get(discord_api_types_v10.Routes.currentApplication())).id;
1080
+ } catch (error) {
1081
+ throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreApplicationUnavailable, { cause: error });
1082
+ }
1083
+ }
1084
+
1085
+ //#endregion
1086
+ //#region src/emojis/EmojiInjector.ts
1087
+ var Emoji = class {
1088
+ name;
1089
+ id;
1090
+ animated;
1091
+ constructor(name, id, animated = false) {
1092
+ this.name = name;
1093
+ this.id = id;
1094
+ this.animated = animated;
1095
+ }
1096
+ toString() {
1097
+ return `<${this.animated ? "a" : ""}:${this.name}:${this.id}>`;
1098
+ }
1099
+ };
1100
+ const emojiStorage = (0, _seedcord_core_internal.accessorStore)();
1101
+ function reasonOf(error) {
1102
+ return Error.isError(error) ? error.message : String(error);
1103
+ }
1104
+ function byName(list) {
1105
+ const map = /* @__PURE__ */ new Map();
1106
+ for (const entry of list) if (entry.name) map.set(entry.name, entry);
1107
+ return map;
1108
+ }
1109
+ /**
1110
+ * The bot's resolved emojis, keyed by {@link EmojiMap}. Filled by {@link EmojiInjector} during startup.
1111
+ * A read before that throws.
1112
+ */
1113
+ const Emojis = (0, _seedcord_core_internal.guardedAccessor)("Emojis", emojiStorage);
1114
+ /**
1115
+ * Resolves the configured emojis over REST at startup.
1116
+ *
1117
+ * @internal
1118
+ */
1119
+ var EmojiInjector = class {
1120
+ core;
1121
+ applicationId;
1122
+ logger = new _seedcord_logger.Logger("Emojis", { channel: "bot" });
1123
+ constructor(core, applicationId = () => fetchApplicationId(core.rest)) {
1124
+ this.core = core;
1125
+ this.applicationId = applicationId;
1126
+ }
1127
+ async init() {
1128
+ (0, _seedcord_core_internal.clearStore)(emojiStorage);
1129
+ const configured = this.core.config.bot.emojis;
1130
+ if (!configured || Object.keys(configured).length === 0) {
1131
+ this.logger.debug("No emojis configured, skipping emoji injection.");
1132
+ return;
1133
+ }
1134
+ const failures = [];
1135
+ const guilds = /* @__PURE__ */ new Map();
1136
+ let application;
1137
+ for (const [key, value] of Object.entries(configured)) if ((0, _seedcord_core_internal.isEmojiTuple)(value)) await this.resolveTuple(key, value, guilds, failures);
1138
+ else if (typeof value === "string") {
1139
+ application ??= await this.applicationEmojis(failures);
1140
+ this.resolve(key, value, application, failures, "the application");
1141
+ } else failures.push(` - "${key}" has an invalid value (expected a name or [name, guildId])`);
1142
+ if (failures.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigEmojiUnresolved, [failures.length, failures.join("\n")]);
1143
+ this.logger.utils.summary("Loaded emojis", { emojis: Object.keys(emojiStorage).length });
1144
+ }
1145
+ async applicationEmojis(failures) {
1146
+ try {
1147
+ const appId = await this.applicationId();
1148
+ return byName((await this.core.rest.get(discord_api_types_v10.Routes.applicationEmojis(appId))).items);
1149
+ } catch (error) {
1150
+ failures.push(` - the application emojis could not be read (${reasonOf(error)})`);
1151
+ return /* @__PURE__ */ new Map();
1152
+ }
1153
+ }
1154
+ async resolveTuple(key, [name, guildId], guilds, failures) {
1155
+ let guild = guilds.get(guildId);
1156
+ if (!guild) {
1157
+ try {
1158
+ guild = byName(await this.core.rest.get(discord_api_types_v10.Routes.guildEmojis(guildId)));
1159
+ } catch (error) {
1160
+ failures.push(` - "${name}" for "${key}" targets guild ${guildId}, which could not be read (${reasonOf(error)})`);
1161
+ return;
1162
+ }
1163
+ guilds.set(guildId, guild);
1164
+ }
1165
+ this.resolve(key, name, guild, failures, `guild ${guildId}`);
1166
+ }
1167
+ resolve(key, name, source, failures, where) {
1168
+ const found = source.get(name);
1169
+ if (!found?.id) {
1170
+ failures.push(` - "${name}" for "${key}" was not found in ${where}`);
1171
+ return;
1172
+ }
1173
+ emojiStorage[key] = new Emoji(name, found.id, found.animated ?? false);
1174
+ }
1175
+ };
1176
+
836
1177
  //#endregion
837
1178
  //#region src/handlers/interaction/SlashHandler.ts
838
1179
  /**
@@ -1013,12 +1354,12 @@ var SelectMenuHandler = class extends ComponentHandler {};
1013
1354
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1014
1355
  */
1015
1356
  var ModalHandler = class extends ComponentHandler {
1016
- /** Rewrite the message this modal was opened from (only when a component opened it). */
1357
+ /** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
1017
1358
  update(response) {
1018
1359
  this.ensureSourceMessage("update");
1019
1360
  return super.update(response);
1020
1361
  }
1021
- /** Silently acknowledge, leaving the source message untouched. */
1362
+ /** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
1022
1363
  deferUpdate() {
1023
1364
  this.ensureSourceMessage("deferUpdate");
1024
1365
  return super.deferUpdate();
@@ -1029,6 +1370,98 @@ var ModalHandler = class extends ComponentHandler {
1029
1370
  }
1030
1371
  };
1031
1372
 
1373
+ //#endregion
1374
+ //#region src/subscribers/index.ts
1375
+ /**
1376
+ * Base class for an HTTP subscriber, binding `this.core` to the HTTP {@link Core}.
1377
+ *
1378
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
1379
+ */
1380
+ var Subscriber = class extends _seedcord_core.Subscriber {};
1381
+ /**
1382
+ * Base class for an HTTP subscriber that delivers its event to a Discord webhook, binding
1383
+ * `this.core` to the HTTP {@link Core}.
1384
+ *
1385
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1386
+ */
1387
+ var WebhookLog = class extends _seedcord_core.WebhookLog {};
1388
+
1389
+ //#endregion
1390
+ //#region src/pagination/Paginator.ts
1391
+ function contextOf(interaction, core) {
1392
+ return {
1393
+ interaction,
1394
+ user: interaction.member?.user ?? interaction.user,
1395
+ guildId: interaction.guild_id ?? null,
1396
+ core
1397
+ };
1398
+ }
1399
+ /**
1400
+ * A restart-proof paginator. Each nav button's customId encodes its full target page, so clicks are
1401
+ * idempotent and survive a restart. A persistent `@ButtonRoute` on `Handler` dispatches the clicks.
1402
+ *
1403
+ * @typeParam Item - The item type, inferred from the source.
1404
+ * @typeParam Prefix - The route prefix, inferred from `config.prefix`.
1405
+ *
1406
+ * @example
1407
+ * ```ts
1408
+ * export const Bans = new Paginator({
1409
+ * prefix: 'bans',
1410
+ * source: new ArraySource((ctx) => loadBans(ctx.guildId), { perPage: 10 }),
1411
+ * renderItem: (ban) => ban.user.username
1412
+ * });
1413
+ *
1414
+ * \@ButtonRoute(Bans.cursor)
1415
+ * export class BansNav extends Bans.Handler {}
1416
+ * ```
1417
+ */
1418
+ var Paginator = class extends _seedcord_core.PaginatorBase {
1419
+ /** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
1420
+ Handler;
1421
+ constructor(config) {
1422
+ super(config);
1423
+ const loadPage = (ctx, n) => this.page(ctx, n);
1424
+ this.Handler = class Nav extends ButtonHandler {
1425
+ async execute() {
1426
+ await this.deferUpdate();
1427
+ const response = await loadPage(contextOf(this.event, this.core), this.params.page);
1428
+ await this.update(response);
1429
+ }
1430
+ };
1431
+ }
1432
+ /**
1433
+ * Render page 0 and send it through the handler's sender, which picks reply, edit, or followUp from its
1434
+ * ack state.
1435
+ *
1436
+ * @param handler - The handler starting the paginator, normally `this`.
1437
+ */
1438
+ async start(handler) {
1439
+ const interaction = handler.getEvent();
1440
+ const response = await this.page(contextOf(interaction, handler.core), 0);
1441
+ return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1442
+ }
1443
+ };
1444
+
1445
+ //#endregion
1446
+ //#region src/pagination/sources.ts
1447
+ /**
1448
+ * A source for a bounded list you can load whole. It loads the full list on every click, then slices the
1449
+ * page, so the real total is known (a real last button, "Page X of Y"). Cache inside your loader if the
1450
+ * load is expensive.
1451
+ *
1452
+ * @typeParam Item - The item type, inferred from the loader's return.
1453
+ */
1454
+ var ArraySource = class extends _seedcord_core.ArraySource {};
1455
+ /**
1456
+ * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
1457
+ * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
1458
+ * source has no cheap total, so `totalPages` is undefined, the last button is omitted, and the indicator
1459
+ * reads "Page X".
1460
+ *
1461
+ * @typeParam Item - The item type, inferred from the fetcher's slice.
1462
+ */
1463
+ var CursorSource = class extends _seedcord_core.CursorSource {};
1464
+
1032
1465
  //#endregion
1033
1466
  //#region src/gates/Gated.ts
1034
1467
  /**
@@ -1073,9 +1506,15 @@ function Gated(...gates) {
1073
1506
  //#endregion
1074
1507
  //#region src/version.ts
1075
1508
  /** Package version */
1076
- const version = "0.1.0-next.1";
1509
+ const version = "0.1.0-next.3";
1077
1510
 
1078
1511
  //#endregion
1512
+ Object.defineProperty(exports, 'ArraySource', {
1513
+ enumerable: true,
1514
+ get: function () {
1515
+ return ArraySource;
1516
+ }
1517
+ });
1079
1518
  Object.defineProperty(exports, 'AutocompleteHandler', {
1080
1519
  enumerable: true,
1081
1520
  get: function () {
@@ -1100,6 +1539,24 @@ Object.defineProperty(exports, 'ContextMenuHandler', {
1100
1539
  return ContextMenuHandler;
1101
1540
  }
1102
1541
  });
1542
+ Object.defineProperty(exports, 'CursorSource', {
1543
+ enumerable: true,
1544
+ get: function () {
1545
+ return CursorSource;
1546
+ }
1547
+ });
1548
+ Object.defineProperty(exports, 'EmojiInjector', {
1549
+ enumerable: true,
1550
+ get: function () {
1551
+ return EmojiInjector;
1552
+ }
1553
+ });
1554
+ Object.defineProperty(exports, 'Emojis', {
1555
+ enumerable: true,
1556
+ get: function () {
1557
+ return Emojis;
1558
+ }
1559
+ });
1103
1560
  Object.defineProperty(exports, 'Gated', {
1104
1561
  enumerable: true,
1105
1562
  get: function () {
@@ -1118,6 +1575,12 @@ Object.defineProperty(exports, 'ModalHandler', {
1118
1575
  return ModalHandler;
1119
1576
  }
1120
1577
  });
1578
+ Object.defineProperty(exports, 'Paginator', {
1579
+ enumerable: true,
1580
+ get: function () {
1581
+ return Paginator;
1582
+ }
1583
+ });
1121
1584
  Object.defineProperty(exports, 'RepliableHandler', {
1122
1585
  enumerable: true,
1123
1586
  get: function () {
@@ -1136,6 +1599,18 @@ Object.defineProperty(exports, 'SlashHandler', {
1136
1599
  return SlashHandler;
1137
1600
  }
1138
1601
  });
1602
+ Object.defineProperty(exports, 'Subscriber', {
1603
+ enumerable: true,
1604
+ get: function () {
1605
+ return Subscriber;
1606
+ }
1607
+ });
1608
+ Object.defineProperty(exports, 'WebhookLog', {
1609
+ enumerable: true,
1610
+ get: function () {
1611
+ return WebhookLog;
1612
+ }
1613
+ });
1139
1614
  Object.defineProperty(exports, '__exportAll', {
1140
1615
  enumerable: true,
1141
1616
  get: function () {
@@ -1148,6 +1623,12 @@ Object.defineProperty(exports, '__reExport', {
1148
1623
  return __reExport;
1149
1624
  }
1150
1625
  });
1626
+ Object.defineProperty(exports, '__toESM', {
1627
+ enumerable: true,
1628
+ get: function () {
1629
+ return __toESM;
1630
+ }
1631
+ });
1151
1632
  Object.defineProperty(exports, 'buildEngine', {
1152
1633
  enumerable: true,
1153
1634
  get: function () {
@@ -1166,10 +1647,16 @@ Object.defineProperty(exports, 'createSeedcord', {
1166
1647
  return createSeedcord;
1167
1648
  }
1168
1649
  });
1650
+ Object.defineProperty(exports, 'fetchApplicationId', {
1651
+ enumerable: true,
1652
+ get: function () {
1653
+ return fetchApplicationId;
1654
+ }
1655
+ });
1169
1656
  Object.defineProperty(exports, 'version', {
1170
1657
  enumerable: true,
1171
1658
  get: function () {
1172
1659
  return version;
1173
1660
  }
1174
1661
  });
1175
- //# sourceMappingURL=edge.index-D-60VfMn.cjs.map
1662
+ //# sourceMappingURL=edge.index-Bd6sbr37.cjs.map