meocord 4.2.2 → 4.2.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.
Files changed (136) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
  3. package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
  4. package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
  5. package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +432 -181
  9. package/dist/cjs/decorator/index.cjs +85 -120
  10. package/dist/cjs/testing/index.cjs +1664 -784
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/package.json.template +9 -9
  13. package/dist/esm/bin/app-template/src/main.ts.template +3 -2
  14. package/dist/esm/bin/generator.js +6 -4
  15. package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
  16. package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
  17. package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
  18. package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
  19. package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
  20. package/dist/esm/bin/helper/service-generator.helper.js +6 -4
  21. package/dist/esm/bin/meocord.js +106 -48
  22. package/dist/esm/build/native-addons.js +75 -19
  23. package/dist/esm/build/rsbuild-config.js +11 -3
  24. package/dist/esm/build/typed-externals.js +15 -2
  25. package/dist/esm/common/cooldown-store.js +38 -14
  26. package/dist/esm/common/deprecation.js +4 -1
  27. package/dist/esm/common/execution-context.js +3 -1
  28. package/dist/esm/common/explained-error.js +7 -3
  29. package/dist/esm/common/log-level.js +13 -9
  30. package/dist/esm/common/logger.js +4 -2
  31. package/dist/esm/common/meocord-text.js +11 -6
  32. package/dist/esm/common/redis-cooldown-store.js +3 -7
  33. package/dist/esm/common/response/flags.js +17 -1
  34. package/dist/esm/common/response/install-context.js +27 -16
  35. package/dist/esm/common/response/presenter.js +7 -3
  36. package/dist/esm/common/response/response-state.js +27 -5
  37. package/dist/esm/common/sharded-cooldown-store.js +11 -9
  38. package/dist/esm/common/theme.js +3 -1
  39. package/dist/esm/common/translator.js +17 -8
  40. package/dist/esm/core/autocomplete-routes.js +26 -0
  41. package/dist/esm/core/command-conflicts.js +15 -7
  42. package/dist/esm/core/command-registration.js +15 -1
  43. package/dist/esm/core/cooldown-runner.js +7 -9
  44. package/dist/esm/core/defer.js +2 -5
  45. package/dist/esm/core/dispatcher.js +83 -58
  46. package/dist/esm/core/event-requirements.js +67 -19
  47. package/dist/esm/core/event-sources.js +100 -0
  48. package/dist/esm/core/fallback.js +3 -1
  49. package/dist/esm/core/filter-runner.js +4 -4
  50. package/dist/esm/core/guard-runner.js +173 -52
  51. package/dist/esm/core/handler-input.js +24 -3
  52. package/dist/esm/core/handler-pipeline.js +44 -28
  53. package/dist/esm/core/handler-registry.js +51 -21
  54. package/dist/esm/core/input-runner.js +7 -8
  55. package/dist/esm/core/interceptor-runner.js +4 -5
  56. package/dist/esm/core/lifecycle-hooks.js +3 -8
  57. package/dist/esm/core/lifecycle-order.js +1 -15
  58. package/dist/esm/core/meocord-classes.js +14 -1
  59. package/dist/esm/core/meocord-factory.js +80 -53
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +5 -9
  62. package/dist/esm/core/message-params.js +126 -46
  63. package/dist/esm/core/message-routes.js +225 -45
  64. package/dist/esm/core/message-words.js +30 -2
  65. package/dist/esm/core/observer-runner.js +2 -1
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +28 -3
  68. package/dist/esm/core/scalar-types.js +57 -16
  69. package/dist/esm/core/shard-context.js +11 -10
  70. package/dist/esm/core/shard-exit.js +3 -3
  71. package/dist/esm/core/shard-manager.js +16 -6
  72. package/dist/esm/core/startup-roots.js +28 -8
  73. package/dist/esm/core/theme-resolvers.js +26 -11
  74. package/dist/esm/core/theme-runtime.js +23 -13
  75. package/dist/esm/core/theme-scope.js +27 -29
  76. package/dist/esm/core/theme-validation.js +1 -1
  77. package/dist/esm/decorator/app.decorator.js +6 -1
  78. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  79. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  80. package/dist/esm/decorator/controller.decorator.js +48 -19
  81. package/dist/esm/decorator/defer.decorator.js +2 -2
  82. package/dist/esm/decorator/event.decorator.js +4 -5
  83. package/dist/esm/decorator/filter.decorator.js +2 -2
  84. package/dist/esm/decorator/guard.decorator.js +4 -61
  85. package/dist/esm/decorator/inject.decorator.js +4 -1
  86. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  87. package/dist/esm/decorator/theme.decorator.js +4 -3
  88. package/dist/esm/decorator/validation.decorator.js +5 -5
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +29 -13
  91. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  92. package/dist/esm/testing/discord-defaults.js +449 -355
  93. package/dist/esm/testing/inspect-handler.js +91 -10
  94. package/dist/esm/testing/meocord-testing-module.js +195 -77
  95. package/dist/esm/testing/mock-fn.js +8 -2
  96. package/dist/esm/testing/mock-interaction.js +639 -113
  97. package/dist/esm/testing/response-log.js +8 -0
  98. package/dist/esm/testing/response.js +7 -5
  99. package/dist/esm/testing/routing.js +48 -6
  100. package/dist/esm/testing/strict-mocks.js +1 -0
  101. package/dist/esm/testing/theme.js +3 -2
  102. package/dist/esm/util/common.util.js +30 -9
  103. package/dist/esm/util/generator-cli.util.js +6 -4
  104. package/dist/esm/util/inherited-env.util.js +72 -13
  105. package/dist/esm/util/json.util.js +10 -3
  106. package/dist/esm/util/meocord-cli.util.js +3 -2
  107. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  108. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  109. package/dist/esm/util/meocord-source-config.util.js +13 -3
  110. package/dist/esm/util/platform.util.js +3 -2
  111. package/dist/esm/util/refusal.util.js +29 -11
  112. package/dist/esm/util/registration-mode.util.js +14 -1
  113. package/dist/esm/util/timer-limit.util.js +4 -1
  114. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  115. package/dist/esm/util/tsconfig.util.js +16 -58
  116. package/dist/esm/util/user-text.util.js +8 -4
  117. package/dist/esm/util/value.util.js +15 -4
  118. package/dist/types/common/index.d.cts +13 -9
  119. package/dist/types/common/index.d.ts +13 -9
  120. package/dist/types/core/index.d.cts +21 -11
  121. package/dist/types/core/index.d.ts +21 -11
  122. package/dist/types/decorator/index.d.cts +63 -63
  123. package/dist/types/decorator/index.d.ts +63 -63
  124. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  125. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  126. package/dist/types/interface/index.d.cts +1 -1
  127. package/dist/types/interface/index.d.ts +1 -1
  128. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  129. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  130. package/dist/types/testing/index.d.cts +137 -54
  131. package/dist/types/testing/index.d.ts +137 -54
  132. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  133. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  134. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  135. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  136. package/package.json +5 -2
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var discord_js = require('discord.js');
4
- var controller_decorator = require('./controller.decorator-U2NBWcoU.cjs');
5
- var providers = require('./providers-B4MOFBrW.cjs');
4
+ var controller_decorator = require('./controller.decorator-hp5xdJU4.cjs');
5
+ var providers = require('./providers-BBvbnmLP.cjs');
6
6
  var enum_index = require('../enum/index.cjs');
7
7
  var node_async_hooks = require('node:async_hooks');
8
- var translateError = require('./translate-error-CbBHUu5P.cjs');
8
+ var translateError = require('./translate-error-wsgfsUET.cjs');
9
9
 
10
10
  /**
11
11
  * How a pattern ranks, one character per segment twice over: `L` for literal text or `P` for a param, then how narrow
@@ -392,9 +392,9 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
392
392
  }
393
393
  /**
394
394
  * Warns about a handler a subclass re-declares on other routes while it still answers the ones it inherits, for
395
- * `@Command`, `@MessageHandler`, `@ReactionHandler` and `@Autocomplete` alike. The next major version (5.0) drops
396
- * the inherited routes. Every class a listed controller extends is checked, a base that is not listed included, for
397
- * the methods the controller still inherits through it.
395
+ * `@Command`, `@MessageHandler`, `@ReactionHandler`, `@Autocomplete`, `@On` and `@Once` alike. The next major version
396
+ * (5.0) drops the inherited routes. Every class a listed controller extends is checked, a base that is not listed
397
+ * included, for the methods the controller still inherits through it.
398
398
  */ function warnInheritedRoutes(controllerClasses) {
399
399
  // A set, so a class two listed controllers extend is named once, for the routes either of them still answers
400
400
  const problems = new Set();
@@ -403,11 +403,18 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
403
403
  // nothing of them from above it, so a class above is not named for them
404
404
  const cut = new Set();
405
405
  for(let prototype = controllerClass.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
406
- const declared = controller_decorator.getDeclaredRoutes(prototype);
407
406
  const base = Object.getPrototypeOf(prototype);
408
- if (declared.length === 0 || !base) continue;
407
+ if (!base) continue;
408
+ const declared = [
409
+ ...controller_decorator.getDeclaredRoutes(prototype),
410
+ ...controller_decorator.ownEventHandlers(prototype).map(controller_decorator.eventRoute)
411
+ ];
412
+ if (declared.length === 0) continue;
409
413
  if (Reflect.getOwnMetadata(controller_decorator.META.replacesInheritedRoutes, prototype)) for (const route of declared)cut.add(route.method);
410
- const inherited = controller_decorator.getHandlerRoutes(base).filter((route)=>!cut.has(route.method));
414
+ const inherited = [
415
+ ...controller_decorator.getHandlerRoutes(base),
416
+ ...providers.getEventHandlers(base).map(controller_decorator.eventRoute)
417
+ ].filter((route)=>!cut.has(route.method));
411
418
  for (const method of new Set(declared.map((route)=>route.method))){
412
419
  const own = [
413
420
  ...new Set(declared.filter((route)=>route.method === method).map((route)=>route.label))
@@ -721,7 +728,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
721
728
  parse: []
722
729
  }
723
730
  }));
724
- if (seconds > 0) {
731
+ // Longer than a timer can wait keeps it, as 0 does; the timer would fire at once
732
+ if (seconds > 0 && providers.timerKeepsSeconds(seconds)) {
725
733
  setTimeout(()=>{
726
734
  reply.delete().catch((failure)=>controller_decorator.logFailedSend(logger, 'delete a reply to a command', failure));
727
735
  }, seconds * 1000).unref?.();
@@ -919,9 +927,9 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
919
927
  }
920
928
  /**
921
929
  * Resolves each unit and runs its `onReady` one at a time, in the units' dependency order. A failure is
922
- * reported and the next unit still runs. Each unit whose `onReady` settled, or that has none, is pushed
923
- * to `entries` as it finishes, so a shutdown that starts meanwhile closes exactly those.
924
- */ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled, runOnReady } = {}) {
930
+ * reported and the next unit still runs. `settled` hears of each unit as its `onReady` settles, or once it is found
931
+ * to have none, which is what a shutdown that starts meanwhile reads.
932
+ */ async function runReadyHooks(container, units, client, info, report, { stopped = ()=>false, slowAfterMs, settled, runOnReady } = {}) {
925
933
  const failed = new Set();
926
934
  // For each unit, the failed units it depends on, directly or through another dependency
927
935
  const failedUpstream = new Map();
@@ -958,11 +966,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
958
966
  continue;
959
967
  }
960
968
  firstToken.set(instance, unit.token);
961
- const entry = lifecycleEntry(unit, instance);
962
- // Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready waits for the one still
963
- // starting, within its time, and skips those not reached yet
964
969
  if (typeof instance.onReady !== 'function') {
965
- entries.push(entry);
966
970
  settled?.(unit);
967
971
  continue;
968
972
  }
@@ -979,7 +983,6 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
979
983
  } finally{
980
984
  clearTimeout(slow);
981
985
  }
982
- entries.push(entry);
983
986
  settled?.(unit);
984
987
  }
985
988
  }
@@ -1093,6 +1096,28 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1093
1096
  ]);
1094
1097
  }
1095
1098
 
1099
+ /**
1100
+ * Every `@Autocomplete` handler of the given controllers, read from their metadata, with the handlers of one option
1101
+ * before those of a whole command, so an option's handler wins over its command's whatever order they are declared in.
1102
+ */ function buildAutocompleteRoutes(controllers) {
1103
+ const routes = controllers.flatMap((controllerClass)=>controller_decorator.getAutocompleteHandlers(controllerClass.prototype).map((meta)=>({
1104
+ controllerClass,
1105
+ meta
1106
+ })));
1107
+ return routes.sort((a, b)=>Number(Boolean(b.meta.optionName)) - Number(Boolean(a.meta.optionName)));
1108
+ }
1109
+ /**
1110
+ * The handler an autocomplete reaches, as dispatch routes it: its full command path, then its command's name, and at
1111
+ * each the first handler whose option is the focused one, or that names none.
1112
+ */ function matchAutocompleteRoute(routes, interaction) {
1113
+ const focused = controller_decorator.focusedOptionName(interaction);
1114
+ for (const path of controller_decorator.resolveCommandPaths(interaction)){
1115
+ const route = routes.find(({ meta })=>meta.commandPath === path && (meta.optionName === undefined || meta.optionName === focused));
1116
+ if (route) return route;
1117
+ }
1118
+ return undefined;
1119
+ }
1120
+
1096
1121
  /** The keys a command is routed by, most specific first: a chat command's full path, then its name; none for anything else. */ function commandRouteKeys(interaction) {
1097
1122
  if (interaction.isChatInputCommand()) return controller_decorator.resolveCommandPaths(interaction);
1098
1123
  if (interaction.isContextMenuCommand() || interaction.isPrimaryEntryPointCommand()) return [
@@ -1255,6 +1280,26 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1255
1280
  if (winner) return `command '${keys[0]}' does not reach ${handler}: dispatch runs ${winner.controllerClass.name}.${winner.meta.methodName}.`;
1256
1281
  return `command '${keys[0]}' does not match ${handler}'s route ${named.map(({ route })=>`'${route}'`).join(' or ')}.`;
1257
1282
  }
1283
+ /**
1284
+ * Why an autocomplete could not reach a handler, for a test calling the handler directly: the handler is checked
1285
+ * against the one dispatch routes the autocomplete to among `controllers`, by its command path and focused option.
1286
+ * `undefined` when it is that one, when the handler has no `@Autocomplete` route, or when the interaction is not an
1287
+ * autocomplete or has no command name.
1288
+ */ function autocompleteMismatch(controllers, controller, methodName, interaction) {
1289
+ if (!interaction.isAutocomplete() || typeof interaction.commandName !== 'string') return undefined;
1290
+ const own = controller_decorator.getAutocompleteHandlers(controller.prototype).filter((meta)=>meta.methodName === methodName);
1291
+ if (own.length === 0) return undefined;
1292
+ const winner = matchAutocompleteRoute(buildAutocompleteRoutes(controllers), interaction);
1293
+ if (winner?.controllerClass === controller && winner.meta.methodName === methodName) return undefined;
1294
+ const handler = `${controller.name}.${methodName}`;
1295
+ const received = controller_decorator.describeInteraction(interaction);
1296
+ // A mock without a focused option reaches no option's handler, the commonest way to miss one
1297
+ const option = controller_decorator.focusedOptionName(interaction) === undefined ? own.find((meta)=>meta.optionName !== undefined)?.optionName : undefined;
1298
+ const fix = option === undefined ? '' : ` Give the mock the focused option: options: createChatInputOptions({ focused: '${option}' }).`;
1299
+ if (winner) return `${received} does not reach ${handler}: dispatch runs ${winner.controllerClass.name}.${winner.meta.methodName}.${fix}`;
1300
+ const routes = own.map((meta)=>meta.optionName === undefined ? `'${meta.commandPath}'` : `'${meta.commandPath}' option '${meta.optionName}'`);
1301
+ return `${received} does not match ${handler}'s route ${routes.join(' or ')}.${fix}`;
1302
+ }
1258
1303
 
1259
1304
  /**
1260
1305
  * How long a component or modal submission no route takes is left to the client's other listeners, such as a
@@ -1322,13 +1367,19 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1322
1367
  this.logger.warn(`messages.help is on, but ${route.controllerClass.name}.${route.method} (${JSON.stringify(route.pattern)}) takes "${shown}", ` + `which runs it instead, so the built-in help never answers. Turn help off, or give the handler another word.`);
1323
1368
  }
1324
1369
  }
1325
- /** The call for one event: the fallback, told of each error that reaches it when there is a recorder. */ callFor(record, startedAt) {
1370
+ /**
1371
+ * The call for one event, stamped as it is made, so every handler it runs reports one start and a duration that
1372
+ * includes routing and any handler that ran before it; the fallback is told of each error that reaches it when there
1373
+ * is a recorder.
1374
+ */ callFor(record) {
1326
1375
  const fallback = record ? async (error, context)=>{
1327
1376
  record.unhandled(error);
1328
- return this.fallback(error, context);
1377
+ const failure = await this.fallback(error, context);
1378
+ if (failure !== undefined) record.unanswered(error, failure);
1379
+ return failure;
1329
1380
  } : this.fallback;
1330
1381
  return {
1331
- startedAt,
1382
+ startedAt: performance.now(),
1332
1383
  fallback,
1333
1384
  record
1334
1385
  };
@@ -1339,32 +1390,35 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1339
1390
  }
1340
1391
  return this.controllerInstancesCache.get(controllerClass);
1341
1392
  }
1393
+ /**
1394
+ * Runs `run` on the controller's instance, resolved as its handler starts. A controller that cannot be made fails
1395
+ * only its own handlers: the error goes to the global filters, then the fallback, and the call goes on without it.
1396
+ */ async withInstance(controllerClass, args, call, run, unresolved = new Set()) {
1397
+ // A controller that failed to resolve for this event is reported once, however many of its handlers it reaches
1398
+ if (unresolved.has(controllerClass)) return;
1399
+ let instance;
1400
+ try {
1401
+ instance = this.getInstance(controllerClass);
1402
+ } catch (error) {
1403
+ unresolved.add(controllerClass);
1404
+ await providers.handleUnroutedError(this.container, args, error, this.runOptions(call));
1405
+ return;
1406
+ }
1407
+ await run(instance);
1408
+ }
1342
1409
  /** The component routes, built on first use; the app calls it as it is created. */ getComponentRoutes() {
1343
1410
  return this.componentRoutes ?? (this.componentRoutes = buildComponentRoutes([
1344
1411
  ...this.controllerClasses
1345
1412
  ]));
1346
1413
  }
1347
1414
  getAutocompleteRoutes() {
1348
- if (this.autocompleteRoutes) return this.autocompleteRoutes;
1349
- const routes = [];
1350
- for (const controllerClass of this.controllerClasses){
1351
- for (const meta of controller_decorator.getAutocompleteHandlers(this.getInstance(controllerClass))){
1352
- routes.push({
1353
- controllerClass,
1354
- meta
1355
- });
1356
- }
1357
- }
1358
- routes.sort((a, b)=>Number(Boolean(b.meta.optionName)) - Number(Boolean(a.meta.optionName)));
1359
- this.autocompleteRoutes = routes;
1360
- return routes;
1415
+ return this.autocompleteRoutes ?? (this.autocompleteRoutes = buildAutocompleteRoutes(this.controllerClasses));
1361
1416
  }
1362
1417
  /**
1363
1418
  * Dispatches an interaction. A failure outside any handler, such as no route matching or resolving
1364
1419
  * the controller, goes to the global filters, then the fallback.
1365
1420
  */ async interaction(interaction, record) {
1366
- // From the moment it arrives, so an observer's duration includes routing
1367
- const call = this.callFor(record, performance.now());
1421
+ const call = this.callFor(record);
1368
1422
  try {
1369
1423
  await this.dispatchInteraction(interaction, call);
1370
1424
  } catch (error) {
@@ -1390,14 +1444,14 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1390
1444
  if (matched) {
1391
1445
  const { route, params } = matched;
1392
1446
  interaction.dynamicParams = params;
1393
- await this.executeCommand(this.getInstance(route.controllerClass), route.meta, interaction, call);
1447
+ await this.executeCommand(route.controllerClass, route.meta, interaction, call);
1394
1448
  return;
1395
1449
  }
1396
1450
  }
1397
1451
  // The same match a test's invoke checks a handler against, so the two cannot disagree
1398
1452
  const command = matchCommandRoute(this.controllerClasses, interaction);
1399
1453
  if (command) {
1400
- await this.executeCommand(this.getInstance(command.controllerClass), command.meta, interaction, call);
1454
+ await this.executeCommand(command.controllerClass, command.meta, interaction, call);
1401
1455
  return;
1402
1456
  }
1403
1457
  // A component or modal no route takes may be a collector's: with another listener on the client, it has the
@@ -1420,21 +1474,17 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1420
1474
  * Answers an autocomplete interaction from the `@Autocomplete` handler that claims it. An unclaimed
1421
1475
  * option gets an empty list and a warning, rather than a menu stuck loading until Discord times out.
1422
1476
  */ async handleAutocomplete(interaction, call) {
1423
- const focusedName = controller_decorator.focusedOptionName(interaction);
1424
- for (const path of controller_decorator.resolveCommandPaths(interaction)){
1425
- for (const { controllerClass, meta } of this.getAutocompleteRoutes()){
1426
- if (meta.commandPath !== path) continue;
1427
- if (meta.optionName !== undefined && meta.optionName !== focusedName) continue;
1428
- const controllerInstance = this.getInstance(controllerClass);
1429
- this.logger.log('[AUTOCOMPLETE]', `[${path}]`, `[${meta.methodName}]`);
1430
- const params = controller_decorator.resolveOptionParams(interaction);
1431
- const ran = await this.invokeHandler(controllerInstance, meta.methodName, [
1432
- interaction,
1433
- params
1434
- ], call);
1435
- if (!ran) await closeAutocomplete(interaction, this.logger);
1436
- return;
1437
- }
1477
+ const route = matchAutocompleteRoute(this.getAutocompleteRoutes(), interaction);
1478
+ if (route) {
1479
+ const { controllerClass, meta } = route;
1480
+ this.logger.log('[AUTOCOMPLETE]', `[${meta.commandPath}]`, `[${meta.methodName}]`);
1481
+ const params = controller_decorator.resolveOptionParams(interaction);
1482
+ const ran = await this.invokeHandler(this.getInstance(controllerClass), meta.methodName, [
1483
+ interaction,
1484
+ params
1485
+ ], call);
1486
+ if (!ran) await closeAutocomplete(interaction, this.logger);
1487
+ return;
1438
1488
  }
1439
1489
  this.logger.warn(`No handler matched ${controller_decorator.describeInteraction(interaction)}. Declare an @Autocomplete handler for it, ` + `or drop setAutocomplete(true) from the option.`);
1440
1490
  await closeAutocomplete(interaction, this.logger);
@@ -1442,19 +1492,22 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1442
1492
  interaction
1443
1493
  ], this.runOptions(call));
1444
1494
  }
1445
- /** Warns, in development, that a modal field or select menu choice is hidden by a customId param of the same name. */ warnCollisions(methodName, names) {
1495
+ /** Warns, in development, that a modal field or select menu choice is hidden by a customId param of the same name. */ warnCollisions(controllerClass, methodName, names) {
1446
1496
  if (process.env.NODE_ENV !== 'development') return;
1497
+ let warned = this.warnedCollisions.get(controllerClass);
1498
+ if (!warned) this.warnedCollisions.set(controllerClass, warned = new Set());
1447
1499
  for (const name of names){
1448
1500
  const key = `${methodName}:${name}`;
1449
- if (this.warnedCollisions.has(key)) continue;
1450
- this.warnedCollisions.add(key);
1451
- this.logger.warn(`"${name}" is both a customId param and a modal field or select menu choice of ${methodName}; the handler receives the customId ` + `param. Rename one to receive both.`);
1501
+ if (warned.has(key)) continue;
1502
+ warned.add(key);
1503
+ this.logger.warn(`"${name}" is both a customId param and a modal field or select menu choice of ${controllerClass.name}.${methodName}; the handler ` + `receives the customId param. Rename one to receive both.`);
1452
1504
  }
1453
1505
  }
1454
1506
  /**
1455
1507
  * Runs a resolved command, shared by both dispatch paths so a pattern-matched
1456
1508
  * component and a named slash command behave identically once the route is chosen.
1457
- */ async executeCommand(controllerInstance, commandMetadata, interaction, call) {
1509
+ */ async executeCommand(controllerClass, commandMetadata, interaction, call) {
1510
+ const controllerInstance = this.getInstance(controllerClass);
1458
1511
  const { methodName, type } = commandMetadata;
1459
1512
  // No interaction-type check here: both callers pick the route with
1460
1513
  // `matchesCommandType` before getting this far, and `@Command` re-checks the
@@ -1462,7 +1515,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1462
1515
  this.logger.log('[INTERACTION]', `[${type}]`, `[${methodName}]`);
1463
1516
  const routeParams = interaction.dynamicParams;
1464
1517
  const { params, collisions } = handlerInput(interaction, routeParams);
1465
- this.warnCollisions(methodName, collisions);
1518
+ this.warnCollisions(controllerClass, methodName, collisions);
1466
1519
  await this.invokeHandler(controllerInstance, methodName, [
1467
1520
  interaction,
1468
1521
  params
@@ -1509,7 +1562,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1509
1562
  * usage through that handler's filters. A bare parent's unguarded subcommands' usage, once the app's guards allow
1510
1563
  * it, and a failure to read the prefixes, go through the global filters, then the fallback; the listeners still run.
1511
1564
  */ async message(message, record) {
1512
- if (message.author.bot || !message.content?.trim()) return;
1565
+ if (message.author.bot || !providers.hasMessageText(message.content)) return;
1513
1566
  const call = this.callFor(record);
1514
1567
  let target;
1515
1568
  try {
@@ -1536,24 +1589,29 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1536
1589
  }
1537
1590
  // The matched handler, then every listener, each resolved from the container only as it starts
1538
1591
  const runs = [];
1592
+ const unresolved = new Set();
1539
1593
  if (target) {
1540
1594
  const { route, params, start, given } = target;
1541
1595
  noteInvocation(message, `${start}${providers.commandWordsOf(route.tokens).join(' ')}`);
1542
1596
  const hooks = providers.messageCommandHooks(route, params, message, start, given, this.messageOptions.types);
1543
1597
  runs.push({
1544
1598
  name: `${route.controllerClass.name}.${route.method}`,
1545
- run: (at)=>this.invokeHandler(this.getInstance(route.controllerClass), route.method, [
1546
- message,
1547
- params
1548
- ], at, hooks)
1599
+ run: (at)=>this.withInstance(route.controllerClass, [
1600
+ message
1601
+ ], at, (instance)=>this.invokeHandler(instance, route.method, [
1602
+ message,
1603
+ params
1604
+ ], at, hooks), unresolved)
1549
1605
  });
1550
1606
  }
1551
1607
  for (const { controllerClass, method } of this.messageListeners){
1552
1608
  runs.push({
1553
1609
  name: `${controllerClass.name}.${method}`,
1554
- run: (at)=>this.invokeHandler(this.getInstance(controllerClass), method, [
1610
+ run: (at)=>this.withInstance(controllerClass, [
1555
1611
  message
1556
- ], at)
1612
+ ], at, (instance)=>this.invokeHandler(instance, method, [
1613
+ message
1614
+ ], at), unresolved)
1557
1615
  });
1558
1616
  }
1559
1617
  if (this.messageOptions.handlers === 'concurrent') await this.runTogether(runs, call);
@@ -1578,7 +1636,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1578
1636
  ...call,
1579
1637
  record: {
1580
1638
  settled: (...report)=>reports[index].push(report),
1581
- unhandled: (error)=>record.unhandled(error)
1639
+ unhandled: (error)=>record.unhandled(error),
1640
+ unanswered: (error, failure)=>record.unanswered(error, failure)
1582
1641
  }
1583
1642
  } : call)));
1584
1643
  for (const report of reports.flat())record?.settled(...report);
@@ -1658,7 +1717,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1658
1717
  const matching = this.controllerClasses.map((controllerClass)=>({
1659
1718
  controllerClass,
1660
1719
  // Handlers for this emoji first, then those for every emoji
1661
- handlers: controller_decorator.getReactionHandlers(this.getInstance(controllerClass)).filter(forEmoji).sort((a, b)=>Number(!a.emoji) - Number(!b.emoji))
1720
+ handlers: controller_decorator.getReactionHandlers(controllerClass.prototype).filter(forEmoji).sort((a, b)=>Number(!a.emoji) - Number(!b.emoji))
1662
1721
  })).filter(({ handlers })=>handlers.length > 0);
1663
1722
  if (matching.length === 0) return;
1664
1723
  // Only asked when a matching handler leaves bots out, since a partial user costs a fetch
@@ -1682,16 +1741,21 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1682
1741
  return;
1683
1742
  }
1684
1743
  for (const { controllerClass, handlers } of runs){
1685
- const controllerInstance = this.getInstance(controllerClass);
1686
- for (const { method } of handlers){
1687
- await this.invokeHandler(controllerInstance, method, [
1744
+ await this.withInstance(controllerClass, [
1745
+ reaction,
1746
+ {
1747
+ user,
1748
+ action
1749
+ }
1750
+ ], call, async (instance)=>{
1751
+ for (const { method } of handlers)await this.invokeHandler(instance, method, [
1688
1752
  reaction,
1689
1753
  {
1690
1754
  user,
1691
1755
  action
1692
1756
  }
1693
1757
  ], call);
1694
- }
1758
+ });
1695
1759
  }
1696
1760
  }
1697
1761
  /**
@@ -1710,7 +1774,10 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1710
1774
  constructor(options){
1711
1775
  this.options = options;
1712
1776
  this.controllerInstancesCache = new Map();
1713
- /** Handler and name pairs already warned about, so a collision warns once, not on every submit or selection. */ this.warnedCollisions = new Set();
1777
+ /**
1778
+ * Method and name pairs already warned about, by controller class, so a collision warns once per handler, not on
1779
+ * every submit or selection. Keyed by the class itself, so two controllers of one name stay apart.
1780
+ */ this.warnedCollisions = new WeakMap();
1714
1781
  /** The handlers already warned about, so each is named once however often it runs. */ this.warnedUnanswered = new Set();
1715
1782
  /** The message handlers already warned about as slow, so each is named once. */ this.warnedSlow = new Set();
1716
1783
  this.container = options.container;
@@ -1725,6 +1792,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1725
1792
  // The app's own help command reads this table too, so it lists the commands dispatch reaches
1726
1793
  if (this.container.isBound(providers.HandlerRegistry)) providers.shareMessageRoutes(this.container.get(providers.HandlerRegistry), this.messageRoutes);
1727
1794
  this.warnUnreachableHelp();
1795
+ // Here, where the routes are built once, rather than in buildMessageRoutes, which startup runs twice
1796
+ for (const warning of providers.messageRouteWarnings(this.messageRoutes, this.messageOptions))this.logger.warn(warning);
1728
1797
  this.messageListeners = this.controllerClasses.flatMap((controllerClass)=>controller_decorator.getMessageHandlers(controllerClass.prototype).filter((handler)=>handler.pattern === undefined).map(({ method })=>({
1729
1798
  controllerClass,
1730
1799
  method
@@ -1808,19 +1877,38 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1808
1877
  seen.add(cls);
1809
1878
  return providers.injectedTokens(cls).some((token)=>token === controller_decorator.ExecutionContext || providers.isAppClassToken(token) && !providers$1.has(token) && !seen.has(token) && reachesContext(token, providers$1, seen));
1810
1879
  }
1811
- /**
1812
- * The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
1813
- * app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
1814
- * made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
1815
- * is not made.
1816
- */ function stageDependencies({ decorators }, providers$1, presenter) {
1880
+ /** The app's classes the stages and presenter inject. */ function stageInjected({ decorators }, presenter) {
1817
1881
  const stages = [
1818
1882
  ...decorators
1819
1883
  ].filter(([, decorator])=>decorator !== '@Controller()').map(([stage])=>stage);
1820
1884
  if (typeof presenter === 'function') stages.push(presenter);
1821
1885
  return [
1822
1886
  ...new Set(stages.flatMap((stage)=>providers.injectedTokens(stage)))
1823
- ].filter(providers.isAppClassToken).filter((cls)=>!reachesContext(cls, providers$1));
1887
+ ].filter(providers.isAppClassToken);
1888
+ }
1889
+ /**
1890
+ * The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
1891
+ * app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
1892
+ * made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
1893
+ * is not made.
1894
+ */ function stageDependencies(startup, providers, presenter) {
1895
+ return stageInjected(startup, presenter).filter((cls)=>!reachesContext(cls, providers));
1896
+ }
1897
+ /**
1898
+ * A warning for each `@On` and `@Once` handler on a class the stages and presenter reach that is made for each call,
1899
+ * as it reaches `ExecutionContext`: no one instance of it receives events.
1900
+ */ function perCallEventWarnings(startup, providers$1, presenter) {
1901
+ const perCall = new Set();
1902
+ // What a class made for each call injects is made for each call too only if it also reaches the context
1903
+ const visit = (cls)=>{
1904
+ if (perCall.has(cls) || providers$1.has(cls) || !reachesContext(cls, providers$1)) return;
1905
+ perCall.add(cls);
1906
+ providers.injectedTokens(cls).filter(providers.isAppClassToken).forEach(visit);
1907
+ };
1908
+ stageInjected(startup, presenter).forEach(visit);
1909
+ return [
1910
+ ...perCall
1911
+ ].flatMap((cls)=>providers.getEventHandlers(cls.prototype).map(({ event, method, once })=>`${cls.name}.${method}: @${once ? 'Once' : 'On'}('${event}') is on a class made for each call, as it injects ` + 'ExecutionContext or a class that does, so it receives no events. The next major version (5.0) refuses it; ' + 'move the handler to a service that does not.'));
1824
1912
  }
1825
1913
  /** How a container checks a guard first met at a call: the guard and what it injects, as the startup checks would. */ function lateGuardCheck(providers$1, translator) {
1826
1914
  return (guard)=>assertStartupClasses({
@@ -1843,6 +1931,100 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1843
1931
  for (const needsOne of classes.filter((cls)=>providers.injectedTokens(cls).includes(controller_decorator.Translator)))controller_decorator.startupError(controller_decorator.missingTranslatorError(needsOne));
1844
1932
  }
1845
1933
 
1934
+ /**
1935
+ * One source for each instance whose lifecycle hooks run, the cooldown store aside, in lifecycle order; read once the
1936
+ * providers are made, as a value's or a factory's class is its instance's. A class `prepared` lacks has its handlers
1937
+ * prepared here, and one a controller's would be refused for is left out, with `warn` naming the refusal.
1938
+ */ function eventSources(container, providers$1, units, prepared, warn) {
1939
+ // The tokens of each class the container makes, bound as itself or given as a provider's `useClass`, the cooldown
1940
+ // store's included: no event reaches it, nor its instance through a factory
1941
+ const classTokens = new Map();
1942
+ for (const { token } of units){
1943
+ const provider = providers$1.get(token);
1944
+ if (provider && !providers.isClassProvider(provider) || !provider && !providers.isAppClassToken(token)) continue;
1945
+ const cls = provider ? provider.useClass : token;
1946
+ classTokens.set(cls, [
1947
+ ...classTokens.get(cls) ?? [],
1948
+ token
1949
+ ]);
1950
+ }
1951
+ const names = new Map(prepared.map((cls)=>[
1952
+ cls.name,
1953
+ cls
1954
+ ]));
1955
+ const handlers = new Map();
1956
+ const handlersOf = (cls)=>{
1957
+ let found = handlers.get(cls);
1958
+ if (!found) {
1959
+ found = prepared.includes(cls) ? providers.getEventHandlers(cls.prototype) : prepareClass(container, cls, names, warn);
1960
+ handlers.set(cls, found);
1961
+ }
1962
+ return found;
1963
+ };
1964
+ const sources = [];
1965
+ const given = new Set();
1966
+ for (const { token, cooldownStore } of units){
1967
+ // The cooldown store is the app's infrastructure, never one of its classes, so no event reaches it
1968
+ if (cooldownStore) continue;
1969
+ const provider = providers$1.get(token);
1970
+ if (!provider || providers.isClassProvider(provider)) {
1971
+ const cls = provider ? provider.useClass : token;
1972
+ const found = handlersOf(cls);
1973
+ if (found.length > 0) sources.push({
1974
+ cls,
1975
+ handlers: found,
1976
+ resolve: ()=>container.get(token)
1977
+ });
1978
+ continue;
1979
+ }
1980
+ // A value, or a factory's, made by now; one instance under several tokens is one source
1981
+ const instance = container.get(token);
1982
+ if (typeof instance !== 'object' || instance === null || given.has(instance)) continue;
1983
+ given.add(instance);
1984
+ const cls = instance.constructor;
1985
+ if (typeof cls !== 'function') continue;
1986
+ const found = handlersOf(cls);
1987
+ if (found.length === 0) continue;
1988
+ // A factory may return the instance the container made for a class unit, whose own source runs it; only units of
1989
+ // the instance's class are resolved, and each resolves on the event through its own source anyway
1990
+ const tokens = classTokens.get(cls) ?? [];
1991
+ const owned = ()=>tokens.some((token)=>container.get(token) === instance);
1992
+ sources.push({
1993
+ cls,
1994
+ handlers: found,
1995
+ resolve: ()=>owned() ? undefined : instance
1996
+ });
1997
+ }
1998
+ return sources;
1999
+ }
2000
+ /** The handlers of `cls` that pass a controller's checks, warning of each other one and why it does not run. */ function prepareClass(container, cls, names, warn) {
2001
+ const declared = providers.getEventHandlers(cls.prototype);
2002
+ if (declared.length === 0) return [];
2003
+ const other = names.get(cls.name);
2004
+ if (!other) names.set(cls.name, cls);
2005
+ // @Once tells classes apart by name, so this class's would share another's
2006
+ const sharesName = other !== undefined && other !== cls && declared.some((handler)=>handler.once);
2007
+ if (sharesName) {
2008
+ refused(warn, cls, `${cls.name}: two classes have this name; @Cooldown and @Once tell classes apart by name, so they would share their counts. Rename one of them.`, declared.filter((handler)=>handler.once));
2009
+ }
2010
+ const errors = new Map();
2011
+ return declared.filter((handler)=>{
2012
+ if (sharesName && handler.once) return false;
2013
+ let found = errors.get(handler.method);
2014
+ if (!found) {
2015
+ found = providers.prepareEventHandler(container, cls, handler.method);
2016
+ errors.set(handler.method, found);
2017
+ const onMethod = declared.filter((other)=>other.method === handler.method && !(sharesName && other.once));
2018
+ for (const error of found)refused(warn, cls, error.message, onMethod);
2019
+ }
2020
+ return found.length === 0;
2021
+ });
2022
+ }
2023
+ function refused(warn, cls, reason, handlers) {
2024
+ const labels = handlers.map(({ event, method, once })=>`@${once ? 'Once' : 'On'}('${event}') in ${cls.name}.${method}`);
2025
+ warn(`${reason} Until the next major version (5.0) refuses it at startup, ${labels.join(', ')} does not run.`);
2026
+ }
2027
+
1846
2028
  exports.DEFAULT_SHUTDOWN_TIMEOUT_MS = DEFAULT_SHUTDOWN_TIMEOUT_MS;
1847
2029
  exports.Dispatcher = Dispatcher;
1848
2030
  exports.ReadyPass = ReadyPass;
@@ -1850,11 +2032,13 @@ exports.RunningCalls = RunningCalls;
1850
2032
  exports.SHUTDOWN_MARGIN_MS = SHUTDOWN_MARGIN_MS;
1851
2033
  exports.assertDistinctCommands = assertDistinctCommands;
1852
2034
  exports.assertStartupClasses = assertStartupClasses;
2035
+ exports.autocompleteMismatch = autocompleteMismatch;
1853
2036
  exports.buildComponentRoutes = buildComponentRoutes;
1854
2037
  exports.callsSettled = callsSettled;
1855
2038
  exports.commandMismatch = commandMismatch;
1856
2039
  exports.componentRouteFor = componentRouteFor;
1857
2040
  exports.createFallback = createFallback;
2041
+ exports.eventSources = eventSources;
1858
2042
  exports.findComponentRouteConflicts = findComponentRouteConflicts;
1859
2043
  exports.handlerInput = handlerInput;
1860
2044
  exports.lateGuardCheck = lateGuardCheck;
@@ -1862,6 +2046,7 @@ exports.lifecycleEntry = lifecycleEntry;
1862
2046
  exports.matchComponentRoute = matchComponentRoute;
1863
2047
  exports.messageDecorator = messageDecorator;
1864
2048
  exports.ownInteractionListener = ownInteractionListener;
2049
+ exports.perCallEventWarnings = perCallEventWarnings;
1865
2050
  exports.reactionDecorator = reactionDecorator;
1866
2051
  exports.readCustomId = readCustomId;
1867
2052
  exports.replyWithUserError = replyWithUserError;