meocord 4.1.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +23 -23
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-CRn9kWob.cjs} +295 -87
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-ChvMqnLd.cjs} +74 -40
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-BdAUTdYs.cjs} +1 -1
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CQo8mSP8.cjs} +158 -72
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-DhDVbT5M.cjs} +12 -3
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +195 -157
  10. package/dist/cjs/decorator/index.cjs +66 -57
  11. package/dist/cjs/testing/index.cjs +736 -307
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/build/load-config.pre-entry.js +4 -1
  21. package/dist/esm/common/decorator.js +6 -6
  22. package/dist/esm/common/deprecation.js +2 -2
  23. package/dist/esm/common/errors.js +2 -2
  24. package/dist/esm/common/execution-context.js +1 -1
  25. package/dist/esm/common/metadata.js +1 -1
  26. package/dist/esm/common/route.js +10 -1
  27. package/dist/esm/common/translate-error.js +1 -1
  28. package/dist/esm/core/command-conflicts.js +34 -34
  29. package/dist/esm/core/component-routes.js +74 -31
  30. package/dist/esm/core/dispatcher.js +51 -7
  31. package/dist/esm/core/filter-runner.js +2 -2
  32. package/dist/esm/core/handler-pipeline.js +27 -14
  33. package/dist/esm/core/handler-registry.js +1 -1
  34. package/dist/esm/core/interceptor-runner.js +2 -2
  35. package/dist/esm/core/meocord-factory.js +196 -157
  36. package/dist/esm/core/message-routes.js +4 -3
  37. package/dist/esm/core/observer-runner.js +4 -3
  38. package/dist/esm/core/providers.js +38 -21
  39. package/dist/esm/core/route-specificity.js +4 -4
  40. package/dist/esm/core/scalar-types.js +15 -3
  41. package/dist/esm/core/shard-context.js +1 -1
  42. package/dist/esm/core/startup-roots.js +2 -3
  43. package/dist/esm/core/theme-runtime.js +2 -2
  44. package/dist/esm/decorator/app.decorator.js +12 -7
  45. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  46. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  47. package/dist/esm/decorator/controller.decorator.js +110 -67
  48. package/dist/esm/decorator/cooldown.decorator.js +3 -3
  49. package/dist/esm/decorator/defer.decorator.js +4 -4
  50. package/dist/esm/decorator/event.decorator.js +5 -5
  51. package/dist/esm/decorator/filter.decorator.js +7 -7
  52. package/dist/esm/decorator/guard.decorator.js +8 -8
  53. package/dist/esm/decorator/inject.decorator.js +4 -4
  54. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  55. package/dist/esm/decorator/observer.decorator.js +4 -4
  56. package/dist/esm/decorator/service.decorator.js +4 -4
  57. package/dist/esm/decorator/theme.decorator.js +3 -3
  58. package/dist/esm/decorator/validation.decorator.js +10 -10
  59. package/dist/esm/package.json.js +1 -1
  60. package/dist/esm/testing/catalog.js +1 -1
  61. package/dist/esm/testing/cooldown-store-suite.js +18 -10
  62. package/dist/esm/testing/discord-defaults.js +60 -7
  63. package/dist/esm/testing/index.js +4 -2
  64. package/dist/esm/testing/inspect-handler.js +20 -3
  65. package/dist/esm/testing/meocord-testing-module.js +229 -188
  66. package/dist/esm/testing/mock-fn.js +80 -10
  67. package/dist/esm/testing/mock-interaction.js +226 -78
  68. package/dist/esm/testing/response.js +1 -1
  69. package/dist/esm/testing/routing.js +39 -14
  70. package/dist/esm/testing/startup-errors.js +26 -0
  71. package/dist/esm/testing/strict-mocks.js +45 -0
  72. package/dist/esm/testing/theme.js +1 -1
  73. package/dist/esm/util/bundle-entry.util.js +2 -1
  74. package/dist/esm/util/interaction.util.js +1 -1
  75. package/dist/esm/util/meocord-config-validation.util.js +1 -0
  76. package/dist/esm/util/metadata-keys.js +1 -0
  77. package/dist/esm/util/refusal.util.js +148 -8
  78. package/dist/types/common/index.d.cts +8 -8
  79. package/dist/types/common/index.d.ts +8 -8
  80. package/dist/types/core/index.d.cts +8 -6
  81. package/dist/types/core/index.d.ts +8 -6
  82. package/dist/types/decorator/index.d.cts +30 -29
  83. package/dist/types/decorator/index.d.ts +30 -29
  84. package/dist/types/{index-D7n0FBMm.d.ts → index-CdXsFgjE.d.ts} +62 -15
  85. package/dist/types/{index-DEP0NYrU.d.cts → index-nn5p7OoX.d.cts} +62 -15
  86. package/dist/types/interface/index.d.cts +1 -1
  87. package/dist/types/interface/index.d.ts +1 -1
  88. package/dist/types/{route-C-aMI7Sl.d.cts → route-BZgFoNeB.d.cts} +5 -2
  89. package/dist/types/{route-C-aMI7Sl.d.ts → route-BZgFoNeB.d.ts} +5 -2
  90. package/dist/types/testing/index.d.cts +245 -31
  91. package/dist/types/testing/index.d.ts +245 -31
  92. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-BcOJVzR9.d.ts} +1 -1
  93. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-UKr3-6Ys.d.cts} +1 -1
  94. package/dist/types/{translator-J6tIUfdp.d.ts → translator-BViWKtLJ.d.ts} +1 -1
  95. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-DHMkkCpg.d.cts} +1 -1
  96. package/package.json +3 -2
@@ -1,13 +1,39 @@
1
1
  'use strict';
2
2
 
3
3
  var discord_js = require('discord.js');
4
- var controller_decorator = require('./controller.decorator-BywwkU9O.cjs');
5
- var providers = require('./providers-BMMLp2SN.cjs');
4
+ var controller_decorator = require('./controller.decorator-CRn9kWob.cjs');
5
+ var providers = require('./providers-ChvMqnLd.cjs');
6
6
  var enum_index = require('../enum/index.cjs');
7
- var translateError = require('./translate-error-BnM8FbSH.cjs');
7
+ var translateError = require('./translate-error-DhDVbT5M.cjs');
8
8
 
9
9
  /**
10
- * Every customId-pattern route of the given controllers, most specific first, read from metadata alone.
10
+ * How a pattern ranks, one character per segment twice over: `L` for literal text or `P` for a param, then how narrow
11
+ * each param is, the narrowest as the lowest digit. Two keys of one length compare as text: literal before param at
12
+ * the first segment where they differ, then the narrower type.
13
+ */ function rankKey(pattern) {
14
+ const narrowness = pattern.split('/').map(controller_decorator.paramNarrowness);
15
+ const kinds = narrowness.map((rank)=>rank === undefined ? 'L' : 'P').join('');
16
+ return `${kinds}/${narrowness.map((rank)=>String(rank === undefined ? 0 : 9 - rank)).join('')}`;
17
+ }
18
+ /**
19
+ * Orders two routes as dispatch tries them. Left to right, the first segment one pattern spells out as literal text
20
+ * while the other leaves it to a param ranks the literal one first: `a/{x}` before `{x}/abcd`, `profile/me/{section}`
21
+ * before `profile/{userId}/edit`. Between patterns that leaves tied, the first param where one type is narrower ranks
22
+ * it first: words to choose from, then `bool`, `uuid`, `snowflake`, `int`, `number`, then text. Patterns of different
23
+ * segment counts, which no customId matches both of, are ordered by count only so the order is total; component
24
+ * patterns have no catch-all param. What remains tied keeps its listing order.
25
+ */ function rank(keys) {
26
+ return (a, b)=>{
27
+ const [left, right] = [
28
+ keys.get(a),
29
+ keys.get(b)
30
+ ];
31
+ if (left.length !== right.length) return left.length - right.length;
32
+ return left < right ? -1 : left > right ? 1 : 0;
33
+ };
34
+ }
35
+ /**
36
+ * Every customId-pattern route of the given controllers, in the order dispatch tries them, read from metadata alone.
11
37
  * Throws for two handlers whose patterns match the same customIds of one component type; one handler
12
38
  * declared under two spellings of a pattern keeps one route.
13
39
  */ function buildComponentRoutes(controllerClasses) {
@@ -40,22 +66,11 @@ var translateError = require('./translate-error-BnM8FbSH.cjs');
40
66
  }
41
67
  }
42
68
  }
43
- // A stable sort, so equally specific patterns keep the order their controllers and handlers are listed in
44
- return routes.sort((a, b)=>(b.meta.specificity ?? 0) - (a.meta.specificity ?? 0));
45
- }
46
- /**
47
- * Whether `a` spells out the first segment where it and `b` differ as literal text while `b` leaves it to a
48
- * parameter, the tie-break between equally specific patterns in the next major version (5.0).
49
- */ function literalFirst(a, b) {
50
- const [left, right] = [
51
- a.split('/'),
52
- b.split('/')
53
- ];
54
- for(let i = 0; i < Math.min(left.length, right.length); i++){
55
- const leftLiteral = !left[i].includes('{');
56
- if (leftLiteral !== !right[i].includes('{')) return leftLiteral;
57
- }
58
- return false;
69
+ // A stable sort, so what the ranking leaves tied keeps the order controllers and handlers are listed in
70
+ return routes.sort(rank(new Map(routes.map((route)=>[
71
+ route,
72
+ rankKey(route.pattern)
73
+ ]))));
59
74
  }
60
75
  /** A component type as an error names it: `button`, `modal submit`, `select menu`. */ const typeLabel = (type)=>type.toLowerCase().replaceAll('_', ' ');
61
76
  /**
@@ -80,6 +95,12 @@ const typesByPattern = new Map();
80
95
  if (!types) typesByPattern.set(pattern, types = controller_decorator.createRegexFromPattern(pattern).types);
81
96
  return types;
82
97
  }
98
+ /**
99
+ * The params `pattern`, compiled as `regex`, captures from a customId, each typed param as its value:
100
+ * `undefined` when the customId does not match, or a segment is not a value of its param's type.
101
+ */ function readCustomId(pattern, regex, customId) {
102
+ return readCustomIdSegments(pattern, regex, customId)?.values;
103
+ }
83
104
  /** {@link readCustomId}'s values, with each param's text as the customId gave it. */ function readCustomIdSegments(pattern, regex, customId) {
84
105
  const match = regex.exec(customId);
85
106
  if (!match) return undefined;
@@ -97,18 +118,40 @@ const typesByPattern = new Map();
97
118
  text
98
119
  };
99
120
  }
100
- /** Pattern pairs that can match one customId, compared only within a component type, as dispatch does. */ function findComponentRouteConflicts(routes) {
101
- const byType = new Map();
102
- for (const { meta, pattern } of routes)byType.set(meta.type, [
103
- ...byType.get(meta.type) ?? [],
104
- pattern
105
- ]);
106
- return [
107
- ...byType
108
- ].flatMap(([type, patterns])=>controller_decorator.findAmbiguousRoutes(patterns).map((pair)=>({
109
- type,
110
- patterns: pair
111
- })));
121
+ /**
122
+ * Pattern pairs of one component type that the ranking leaves tied and that can match one customId, each with the
123
+ * pattern that runs, the one ranked first in `routes`, and an id both match. Only patterns of one rank key, read
124
+ * together, can tie, so each group is compared within itself.
125
+ */ function findComponentRouteConflicts(routes) {
126
+ const groups = new Map();
127
+ for (const route of routes){
128
+ // The literal text and each param's narrowness: patterns that differ in either rank apart or match no id in common
129
+ const key = `${route.meta.type}\0${route.pattern.split('/').map((segment)=>controller_decorator.paramNarrowness(segment) ?? `=${segment}`).join('\0')}`;
130
+ groups.set(key, [
131
+ ...groups.get(key) ?? [],
132
+ route
133
+ ]);
134
+ }
135
+ const conflicts = [];
136
+ for (const group of groups.values()){
137
+ for(let i = 0; i < group.length; i++){
138
+ for(let j = i + 1; j < group.length; j++){
139
+ const customId = controller_decorator.sharedCustomId(group[i].pattern, group[j].pattern);
140
+ if (customId === undefined) continue;
141
+ const patterns = [
142
+ group[i].pattern,
143
+ group[j].pattern
144
+ ];
145
+ conflicts.push({
146
+ type: group[i].meta.type,
147
+ patterns,
148
+ runs: group[i].pattern,
149
+ customId
150
+ });
151
+ }
152
+ }
153
+ }
154
+ return conflicts;
112
155
  }
113
156
 
114
157
  const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.methodName}`;
@@ -131,7 +174,7 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
131
174
  * Refuses commands of which only one could run or be registered: two handlers of one slash command name or subcommand
132
175
  * path or of one context menu name and kind, or two builders of one command; a command's subcommands, and a user and a
133
176
  * message context menu of one name, are no clash. The app, `meocord register`, the shard manager and testing call it.
134
- * @throws Error naming both handlers, or both builders, with what to do instead.
177
+ * Each clash is a startup error, naming both handlers or both builders with what to do instead; see `startupError`.
135
178
  */ function assertDistinctCommands(controllerClasses) {
136
179
  const handlers = new Map();
137
180
  const builders = new Map();
@@ -149,7 +192,8 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
149
192
  const key = `${meta.type}\0${name}`;
150
193
  const earlier = (handlers.get(key) ?? []).find((other)=>overlap(other.meta, meta));
151
194
  if (earlier) {
152
- throw controller_decorator.refuse(new Error(`${where(earlier)}: it and ${where(here)} both handle the ${describe(meta.type, name, contextMenuKind(meta) ?? contextMenuKind(earlier.meta))}, ` + `so only ${where(earlier)} would ever run. Keep one handler for it, or give the other a name or ` + `subcommand path of its own.`));
195
+ controller_decorator.startupError(new Error(`${where(earlier)}: it and ${where(here)} both handle the ${describe(meta.type, name, contextMenuKind(meta) ?? contextMenuKind(earlier.meta))}, ` + `so only ${where(earlier)} would ever run. Keep one handler for it, or give the other a name or ` + `subcommand path of its own.`));
196
+ continue;
153
197
  }
154
198
  handlers.set(key, [
155
199
  ...handlers.get(key) ?? [],
@@ -170,7 +214,7 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
170
214
  name: typeof body.name === 'string' ? body.name : name
171
215
  });
172
216
  } else if (built.meta.builderClass !== meta.builderClass) {
173
- throw controller_decorator.refuse(new Error(`${where(built)}: its builder ${built.meta.builderClass.name} and ${meta.builderClass.name} on ${where(here)} both ` + `build the ${describe(meta.type, built.name, contextMenuKind(meta))}, and Discord registers one command ` + `per name and type, so only the first would be. Keep one builder, on a single @Command, and declare ` + `the other handlers with CommandType.${meta.type}.`));
217
+ controller_decorator.startupError(new Error(`${where(built)}: its builder ${built.meta.builderClass.name} and ${meta.builderClass.name} on ${where(here)} both ` + `build the ${describe(meta.type, built.name, contextMenuKind(meta))}, and Discord registers one command ` + `per name and type, so only the first would be. Keep one builder, on a single @Command, and declare ` + `the other handlers with CommandType.${meta.type}.`));
174
218
  }
175
219
  }
176
220
  }
@@ -348,18 +392,21 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
348
392
  /**
349
393
  * Warns about a handler a subclass re-declares on other routes while it still answers the ones it inherits, for
350
394
  * `@Command`, `@MessageHandler`, `@ReactionHandler` and `@Autocomplete` alike. The next major version (5.0) drops
351
- * the inherited routes. Every class a listed controller extends is checked once, a base that is not listed included.
395
+ * the inherited routes. Every class a listed controller extends is checked, a base that is not listed included, for
396
+ * the methods the controller still inherits through it.
352
397
  */ function warnInheritedRoutes(controllerClasses) {
353
- const checked = new Set();
354
- const problems = [];
398
+ // A set, so a class two listed controllers extend is named once, for the routes either of them still answers
399
+ const problems = new Set();
355
400
  for (const controllerClass of controllerClasses){
401
+ // Methods a class below re-declares with @Controller({ inheritedRoutes: 'replace' }): the listed controller inherits
402
+ // nothing of them from above it, so a class above is not named for them
403
+ const cut = new Set();
356
404
  for(let prototype = controllerClass.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
357
- if (checked.has(prototype)) break;
358
- checked.add(prototype);
359
405
  const declared = controller_decorator.getDeclaredRoutes(prototype);
360
406
  const base = Object.getPrototypeOf(prototype);
361
407
  if (declared.length === 0 || !base) continue;
362
- const inherited = controller_decorator.getHandlerRoutes(base);
408
+ if (Reflect.getOwnMetadata(controller_decorator.META.replacesInheritedRoutes, prototype)) for (const route of declared)cut.add(route.method);
409
+ const inherited = controller_decorator.getHandlerRoutes(base).filter((route)=>!cut.has(route.method));
363
410
  for (const method of new Set(declared.map((route)=>route.method))){
364
411
  const own = [
365
412
  ...new Set(declared.filter((route)=>route.method === method).map((route)=>route.label))
@@ -369,17 +416,19 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
369
416
  ].filter((label)=>!own.includes(label));
370
417
  if (kept.length === 0) continue;
371
418
  const name = prototype.constructor.name;
372
- problems.push(` ${name}.${method} answers ${controller_decorator.listed(kept)}, which it inherits, as well as its own ${controller_decorator.listed(own)}.`);
419
+ problems.add(` ${name}.${method} answers ${controller_decorator.listed(kept)}, which it inherits, as well as its own ${controller_decorator.listed(own)}.`);
373
420
  }
374
421
  }
375
422
  }
376
- if (problems.length === 0) return;
377
- const one = problems.length === 1;
378
- logger.warn(`${problems.length} re-declared ${one ? 'handler still answers routes it inherits' : 'handlers still answer routes they inherit'}:\n` + `${problems.join('\n')}\nIn the next major version (5.0), a handler's own routes replace the ones it inherits. ` + "To keep an inherited route, declare it on the subclass's method as well.");
423
+ if (problems.size === 0) return;
424
+ const one = problems.size === 1;
425
+ logger.warn(`${problems.size} re-declared ${one ? 'handler still answers routes it inherits' : 'handlers still answer routes they inherit'}:\n` + `${[
426
+ ...problems
427
+ ].join('\n')}\nIn the next major version (5.0), a handler's own routes replace the ones it inherits. ` + "To keep an inherited route, declare it on the subclass's method as well; to drop it now, give the subclass " + "@Controller({ inheritedRoutes: 'replace' }).");
379
428
  }
380
429
  /**
381
- * Warns of customId patterns of one component type that can match one customId, naming the handler that runs and why;
382
- * an app whose patterns overlap works, so refusing to start would turn a latent mis-route into an outage.
430
+ * Warns of customId patterns of one component type that rank equally and can match one customId, naming an id both
431
+ * match and the handler that runs. An app with such a pair works, so it starts; the next major version (5.0) refuses.
383
432
  * @throws Error for two handlers whose patterns match the same customIds, which the app refuses as well.
384
433
  */ function warnOverlappingPatterns(controllerClasses) {
385
434
  const routes = buildComponentRoutes(controllerClasses);
@@ -389,23 +438,16 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
389
438
  `${route.meta.type}\0${route.pattern}`,
390
439
  route
391
440
  ]));
392
- const lines = conflicts.map(({ type, patterns: [left, right] })=>{
393
- const outcome = ambiguityOutcome(routeOf.get(`${type}\0${left}`), routeOf.get(`${type}\0${right}`));
394
- return ` "${left}" vs "${right}": ${outcome}`;
395
- });
396
- logger.warn(`${conflicts.length} pattern pair(s) can match the same customId, so which one runs is decided by ranking rather ` + `than by the ids themselves:\n${lines.join('\n')}`);
397
- }
398
- /**
399
- * Which of two overlapping routes runs for the ids both match, and why: `runs` ranks first. Between equally
400
- * specific patterns, the order they are listed in decides until 5.0, which prefers the earlier literal segment.
401
- */ function ambiguityOutcome(runs, other) {
402
441
  const name = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.methodName}`;
403
- if ((runs.meta.specificity ?? 0) !== (other.meta.specificity ?? 0)) return `${name(runs)} runs, as its pattern is more specific.`;
404
- const together = runs.controllerClass === other.controllerClass;
405
- const listed = together ? 'it is declared first' : 'its controller is listed first';
406
- if (!literalFirst(other.pattern, runs.pattern)) return `${name(runs)} runs, as ${listed}.`;
407
- const reorder = together ? `Declare ${name(other)} first` : `List ${other.controllerClass.name} first`;
408
- return `${name(runs)} runs, as ${listed}. In the next major version (5.0), ${name(other)} runs instead, as ` + `"${other.pattern}" spells out the first segment where the two differ. ${reorder}, or make the patterns distinct.`;
442
+ const lines = conflicts.map(({ type, patterns: [left, right], customId })=>{
443
+ const [runs, other] = [
444
+ routeOf.get(`${type}\0${left}`),
445
+ routeOf.get(`${type}\0${right}`)
446
+ ];
447
+ const listed = runs.controllerClass === other.controllerClass ? 'it is declared first' : 'its controller is listed first';
448
+ return ` "${left}" vs "${right}", which both match "${customId}": ${name(runs)} runs, as ${listed}.`;
449
+ });
450
+ logger.warn(`${conflicts.length} pattern pair(s) rank equally and can match the same customId, so the order they are listed in ` + `decides which one runs:\n${lines.join('\n')}\nMake the patterns distinct: in the next major version (5.0), ` + 'MeoCord refuses to start with such a pair.');
409
451
  }
410
452
 
411
453
  const UNKNOWN_INTERACTION = 10062;
@@ -1100,6 +1142,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1100
1142
  if (state && state !== 'unanswered') return true;
1101
1143
  return 'replied' in interaction && (interaction.replied || interaction.deferred);
1102
1144
  }
1145
+ /** How long a message's handler may take, with others waiting after it, before it is named as slow. */ const SLOW_HANDLER_MS = 5000;
1103
1146
  /**
1104
1147
  * Routes interactions, messages and reactions to the handlers that take them, as the bot does, and runs
1105
1148
  * each through its pipeline. The app and the testing module share it, so a test routes exactly as the bot.
@@ -1355,22 +1398,64 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1355
1398
  message
1356
1399
  ], error, this.runOptions(call));
1357
1400
  }
1401
+ // The matched handler, then every listener, each resolved from the container only as it starts
1402
+ const runs = [];
1358
1403
  if (target) {
1359
1404
  const { route, params, start, given } = target;
1360
1405
  noteInvocation(message, `${start}${providers.commandWordsOf(route.tokens).join(' ')}`);
1361
1406
  const hooks = providers.messageCommandHooks(route, params, message, start, given, this.messageOptions.types);
1362
- await this.invokeHandler(this.getInstance(route.controllerClass), route.method, [
1363
- message,
1364
- params
1365
- ], call, hooks);
1407
+ runs.push({
1408
+ name: `${route.controllerClass.name}.${route.method}`,
1409
+ run: (at)=>this.invokeHandler(this.getInstance(route.controllerClass), route.method, [
1410
+ message,
1411
+ params
1412
+ ], at, hooks)
1413
+ });
1366
1414
  }
1367
1415
  for (const { controllerClass, method } of this.messageListeners){
1368
- await this.invokeHandler(this.getInstance(controllerClass), method, [
1369
- message
1370
- ], call);
1416
+ runs.push({
1417
+ name: `${controllerClass.name}.${method}`,
1418
+ run: (at)=>this.invokeHandler(this.getInstance(controllerClass), method, [
1419
+ message
1420
+ ], at)
1421
+ });
1422
+ }
1423
+ if (this.messageOptions.handlers === 'concurrent') await this.runTogether(runs, call);
1424
+ else await this.runInTurn(runs, call);
1425
+ }
1426
+ /** Runs a message's handlers one after another, warning once about one that held the rest back for long. */ async runInTurn(runs, call) {
1427
+ const warn = this.messageOptions.slowHandlerWarning ?? this.options.warnSlowMessageHandlers ?? true;
1428
+ for (const [index, { name, run }] of runs.entries()){
1429
+ const started = performance.now();
1430
+ await run(call);
1431
+ const waiting = runs.length - 1 - index;
1432
+ if (warn && waiting > 0) this.warnSlowOnce(name, performance.now() - started, waiting);
1371
1433
  }
1372
1434
  }
1373
1435
  /**
1436
+ * Starts a message's handlers together and settles once all have. Each is reported as settled in the order the
1437
+ * handlers are listed, whichever settles first, and a call that rejects, rejects this once every one has settled.
1438
+ */ async runTogether(runs, call) {
1439
+ const { record } = call;
1440
+ const reports = runs.map(()=>[]);
1441
+ const results = await Promise.allSettled(runs.map(({ run }, index)=>run(record ? {
1442
+ ...call,
1443
+ record: {
1444
+ settled: (...report)=>reports[index].push(report),
1445
+ unhandled: (error)=>record.unhandled(error)
1446
+ }
1447
+ } : call)));
1448
+ for (const report of reports.flat())record?.settled(...report);
1449
+ const failed = results.find((result)=>result.status === 'rejected');
1450
+ if (failed) throw failed.reason;
1451
+ }
1452
+ /** Warns, once per handler, that it took `SLOW_HANDLER_MS` or longer with `waiting` handlers held back behind it. */ warnSlowOnce(name, took, waiting) {
1453
+ if (took < SLOW_HANDLER_MS || this.warnedSlow.has(name)) return;
1454
+ this.warnedSlow.add(name);
1455
+ const held = waiting === 1 ? 'the listener after it' : `the ${waiting} listeners after it`;
1456
+ this.logger.warn(`${name} took ${(took / 1000).toFixed(1)} s on a message, and held back ${held}. ` + "@MeoCord({ messages: { handlers: 'concurrent' } }) runs them side by side; messages: { slowHandlerWarning: false } turns this off.");
1457
+ }
1458
+ /**
1374
1459
  * Answers a message that asks the built-in help, when `messages.help` is on and no handler took the message:
1375
1460
  * through the presenter's `messageHelp` when it has one, else in plain text, once the app's guards allow it.
1376
1461
  * `known` is the message's starts when they hold the app's prefixes already. Whether it answered, or a
@@ -1491,6 +1576,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1491
1576
  this.controllerInstancesCache = new Map();
1492
1577
  /** Handler and name pairs already warned about, so a collision warns once, not on every submit or selection. */ this.warnedCollisions = new Set();
1493
1578
  /** The handlers already warned about, so each is named once however often it runs. */ this.warnedUnanswered = new Set();
1579
+ /** The message handlers already warned about as slow, so each is named once. */ this.warnedSlow = new Set();
1494
1580
  this.container = options.container;
1495
1581
  this.controllerClasses = options.controllerClasses;
1496
1582
  this.messageOptions = options.messageOptions ?? {};
@@ -1600,8 +1686,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
1600
1686
  /** Runs the startup checks over every class an app runs, so a class that cannot be made is refused by name now. */ function assertStartupClasses({ classes, decorators }, { translator }) {
1601
1687
  providers.assertTypedParameters(classes, decorators);
1602
1688
  if (translator) return;
1603
- const needsOne = classes.find((cls)=>providers.injectedTokens(cls).includes(controller_decorator.Translator));
1604
- if (needsOne) throw controller_decorator.refuse(controller_decorator.missingTranslatorError(needsOne));
1689
+ for (const needsOne of classes.filter((cls)=>providers.injectedTokens(cls).includes(controller_decorator.Translator)))controller_decorator.startupError(controller_decorator.missingTranslatorError(needsOne));
1605
1690
  }
1606
1691
 
1607
1692
  exports.DEFAULT_SHUTDOWN_TIMEOUT_MS = DEFAULT_SHUTDOWN_TIMEOUT_MS;
@@ -1622,6 +1707,7 @@ exports.matchComponentRoute = matchComponentRoute;
1622
1707
  exports.messageDecorator = messageDecorator;
1623
1708
  exports.ownInteractionListener = ownInteractionListener;
1624
1709
  exports.reactionDecorator = reactionDecorator;
1710
+ exports.readCustomId = readCustomId;
1625
1711
  exports.replyWithUserError = replyWithUserError;
1626
1712
  exports.runReadyHooks = runReadyHooks;
1627
1713
  exports.runShutdownSequence = runShutdownSequence;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('./controller.decorator-BywwkU9O.cjs');
3
+ var controller_decorator = require('./controller.decorator-CRn9kWob.cjs');
4
4
  var discord_js = require('discord.js');
5
5
 
6
6
  /** The longest customId Discord accepts. */ const MAX_CUSTOM_ID_LENGTH = 100;
@@ -16,6 +16,10 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
16
16
  decodeSegment(value)
17
17
  ]));
18
18
  }
19
+ /** The segment types whose value is text, which `build` takes only as a string. */ const TEXT_TYPES = new Set([
20
+ 'snowflake',
21
+ 'uuid'
22
+ ]);
19
23
  /**
20
24
  * Makes a typed route from a customId pattern, so one declaration serves the handler and the ids that reach it.
21
25
  *
@@ -50,7 +54,7 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
50
54
  *
51
55
  * @group Utilities
52
56
  * @see {@link Route}
53
- * @see {@link https://meocord.dev/docs/4.1/components | Buttons, selects and modals}
57
+ * @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
54
58
  */ function route(pattern) {
55
59
  const { params, types } = controller_decorator.createRegexFromPattern(pattern);
56
60
  const names = new Set(params);
@@ -62,6 +66,11 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
62
66
  const value = Object.hasOwn(values, name) ? values[name] : undefined;
63
67
  const type = Object.hasOwn(types, name) ? types[name] : undefined;
64
68
  if (value === undefined || value === null) throw new TypeError(`route('${pattern}').build() needs a value for ${placeholder}.`);
69
+ // A snowflake's or uuid's value is its text: a number for a snowflake may have lost digits before it got here
70
+ if (type && TEXT_TYPES.has(type) && typeof value !== 'string') {
71
+ const lost = type === 'snowflake' ? ' A number may have lost digits already: pass the ID as text, such as user.id.' : '';
72
+ throw new TypeError(`route('${pattern}').build() got the ${typeof value} ${String(value)} for ${placeholder}, which takes its value as a string.${lost}`);
73
+ }
65
74
  const text = String(value);
66
75
  if (text === '') throw new TypeError(`route('${pattern}').build() got an empty {${name}}, which no customId segment can hold.`);
67
76
  // A typed segment must read back as the value it was built from, or the route could never match it
@@ -139,7 +148,7 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
139
148
  * @group Utilities
140
149
  * @category Localisation
141
150
  * @see {@link createTranslator}
142
- * @see {@link https://meocord.dev/docs/4.1/localisation | Localisation}
151
+ * @see {@link https://meocord.dev/docs/4.2/localisation | Localisation}
143
152
  */ function translateError(error, t, target) {
144
153
  const locale = target instanceof discord_js.BaseInteraction ? controller_decorator.interactionLocale(target) : target instanceof discord_js.Message ? controller_decorator.messageLocale(target) : target;
145
154
  return errorText(error, t, locale);
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('../_shared/controller.decorator-BywwkU9O.cjs');
3
+ var controller_decorator = require('../_shared/controller.decorator-CRn9kWob.cjs');
4
4
  require('reflect-metadata');
5
- var translateError = require('../_shared/translate-error-BnM8FbSH.cjs');
6
- var shardedCooldownStore = require('../_shared/sharded-cooldown-store-B65iaiAy.cjs');
5
+ var translateError = require('../_shared/translate-error-DhDVbT5M.cjs');
6
+ var shardedCooldownStore = require('../_shared/sharded-cooldown-store-BdAUTdYs.cjs');
7
7
  require('../enum/index.cjs');
8
8
  require('discord.js');
9
9
  require('path');
@@ -150,7 +150,7 @@ const assigned = {};
150
150
  *
151
151
  * @group Utilities
152
152
  * @see {@link ExecutionContext}
153
- * @see {@link https://meocord.dev/docs/4.1/guards | Guards}
153
+ * @see {@link https://meocord.dev/docs/4.2/guards | Guards}
154
154
  */ function createMetadata(description) {
155
155
  const key = Symbol(description);
156
156
  const decorator = (value)=>function(target, propertyKey) {