meocord 4.2.1 → 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 (137) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
  3. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
  4. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
  5. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +435 -182
  9. package/dist/cjs/decorator/index.cjs +116 -121
  10. package/dist/cjs/testing/index.cjs +1923 -808
  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 +32 -10
  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 +27 -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 +92 -66
  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 +6 -6
  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 +54 -32
  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 +83 -54
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +37 -15
  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 +4 -2
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +39 -4
  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/stage-scope.js +2 -1
  73. package/dist/esm/core/startup-roots.js +28 -8
  74. package/dist/esm/core/theme-resolvers.js +26 -11
  75. package/dist/esm/core/theme-runtime.js +23 -13
  76. package/dist/esm/core/theme-scope.js +31 -30
  77. package/dist/esm/core/theme-validation.js +1 -1
  78. package/dist/esm/decorator/app.decorator.js +37 -1
  79. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  80. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  81. package/dist/esm/decorator/controller.decorator.js +48 -19
  82. package/dist/esm/decorator/defer.decorator.js +2 -2
  83. package/dist/esm/decorator/event.decorator.js +4 -5
  84. package/dist/esm/decorator/filter.decorator.js +4 -4
  85. package/dist/esm/decorator/guard.decorator.js +4 -61
  86. package/dist/esm/decorator/inject.decorator.js +4 -1
  87. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  88. package/dist/esm/decorator/theme.decorator.js +4 -3
  89. package/dist/esm/decorator/validation.decorator.js +5 -5
  90. package/dist/esm/package.json.js +1 -1
  91. package/dist/esm/testing/catalog.js +32 -25
  92. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  93. package/dist/esm/testing/discord-defaults.js +449 -335
  94. package/dist/esm/testing/inspect-handler.js +91 -10
  95. package/dist/esm/testing/meocord-testing-module.js +217 -91
  96. package/dist/esm/testing/mock-fn.js +8 -2
  97. package/dist/esm/testing/mock-interaction.js +864 -124
  98. package/dist/esm/testing/response-log.js +8 -0
  99. package/dist/esm/testing/response.js +7 -5
  100. package/dist/esm/testing/routing.js +48 -6
  101. package/dist/esm/testing/strict-mocks.js +8 -4
  102. package/dist/esm/testing/theme.js +3 -2
  103. package/dist/esm/util/common.util.js +30 -9
  104. package/dist/esm/util/generator-cli.util.js +6 -4
  105. package/dist/esm/util/inherited-env.util.js +72 -13
  106. package/dist/esm/util/json.util.js +10 -3
  107. package/dist/esm/util/meocord-cli.util.js +3 -2
  108. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  109. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  110. package/dist/esm/util/meocord-source-config.util.js +13 -3
  111. package/dist/esm/util/platform.util.js +3 -2
  112. package/dist/esm/util/refusal.util.js +29 -11
  113. package/dist/esm/util/registration-mode.util.js +14 -1
  114. package/dist/esm/util/timer-limit.util.js +4 -1
  115. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  116. package/dist/esm/util/tsconfig.util.js +16 -58
  117. package/dist/esm/util/user-text.util.js +8 -4
  118. package/dist/esm/util/value.util.js +33 -5
  119. package/dist/types/common/index.d.cts +17 -10
  120. package/dist/types/common/index.d.ts +17 -10
  121. package/dist/types/core/index.d.cts +21 -11
  122. package/dist/types/core/index.d.ts +21 -11
  123. package/dist/types/decorator/index.d.cts +63 -63
  124. package/dist/types/decorator/index.d.ts +63 -63
  125. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  126. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  127. package/dist/types/interface/index.d.cts +1 -1
  128. package/dist/types/interface/index.d.ts +1 -1
  129. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  130. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  131. package/dist/types/testing/index.d.cts +158 -61
  132. package/dist/types/testing/index.d.ts +158 -61
  133. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  134. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  135. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  136. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  137. package/package.json +5 -2
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
3
+ var controller_decorator = require('./controller.decorator-hp5xdJU4.cjs');
4
4
  require('reflect-metadata');
5
5
  var node_async_hooks = require('node:async_hooks');
6
6
  var inversify = require('inversify');
@@ -11,8 +11,6 @@ var fs = require('fs');
11
11
  var path = require('path');
12
12
  var Table = require('cli-table3');
13
13
  var node_crypto = require('node:crypto');
14
- require('node:fs');
15
- require('node:url');
16
14
 
17
15
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
18
16
 
@@ -90,7 +88,8 @@ var Table__default = /*#__PURE__*/_interopDefaultCompat(Table);
90
88
  }
91
89
  }
92
90
  /** The class of a stage entry. */ function stageClass(entry) {
93
- return controller_decorator.isConstructor(entry) ? entry : entry.provide;
91
+ // Entries were checked as they were declared, so a function is the class itself
92
+ return typeof entry === 'function' ? entry : entry.provide;
94
93
  }
95
94
  /** The context types a stage declared, or `undefined` when it runs for every type. */ function stageTypes(entry) {
96
95
  return Reflect.getMetadata(controller_decorator.META.stageTypes, stageClass(entry));
@@ -103,13 +102,12 @@ function eventDecorator(event, once) {
103
102
  // Generic over the method, so it is checked against the event's arguments rather than required to match exactly
104
103
  return controller_decorator.declaring(function(target, propertyKey, _descriptor) {
105
104
  controller_decorator.refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
106
- const handlers = controller_decorator.ownHandlerList(controller_decorator.META.eventHandlers, target);
107
- handlers.push({
105
+ // One the class inherits for the same method and event is replaced, as for every other kind of handler
106
+ controller_decorator.addOwnHandler(controller_decorator.META.eventHandlers, target, {
108
107
  event,
109
108
  method: propertyKey,
110
109
  once
111
- });
112
- Reflect.defineMetadata(controller_decorator.META.eventHandlers, handlers, target);
110
+ }, (other)=>other.method === propertyKey && other.event === event);
113
111
  });
114
112
  }
115
113
  /**
@@ -305,14 +303,14 @@ const perCallKeys = new WeakMap();
305
303
  });
306
304
  }
307
305
  }
308
- const logger$4 = new controller_decorator.Logger('Guard');
306
+ const logger$5 = new controller_decorator.Logger('Guard');
309
307
  const warnedShared = new WeakMap();
310
308
  /** Warns once per guard in each app that its params are set on the one instance every call shares, and why. */ function warnSharedParams(container, guardClass, why, remedy) {
311
309
  let warned = warnedShared.get(container);
312
310
  if (!warned) warnedShared.set(container, warned = new Set());
313
311
  if (warned.has(guardClass)) return;
314
312
  warned.add(guardClass);
315
- logger$4.warn(`${guardClass.name} is one instance every call shares, and ${why}, so each call's params are set on that ` + `instance: calls that overlap can read each other's. ${remedy}, or stop binding the guard, in services, ` + `providers or a constructor that injects it, so each call gets its own.`);
313
+ logger$5.warn(`${guardClass.name} is one instance every call shares, and ${why}, so each call's params are set on that ` + `instance: calls that overlap can read each other's. ${remedy}, or stop binding the guard, in services, ` + `providers or a constructor that injects it, so each call gets its own.`);
316
314
  }
317
315
  /** Whether each of these properties can be made to read the call's params, which a frozen or sealed instance's cannot. */ function canReadPerCall(instance, keys) {
318
316
  return keys.every((key)=>{
@@ -426,6 +424,28 @@ const warnedShared = new WeakMap();
426
424
  }
427
425
  return undefined;
428
426
  }
427
+ /**
428
+ * The prototypes on the chain that declare `methodName` as written, base first: the handler and each override of it,
429
+ * looking through methods a class-level `@UseGuard` re-declared to wrap an inherited handler. A handler's method-level
430
+ * stages are read from each, so an override keeps its base method's, as it keeps its route.
431
+ */ function stageOwners(prototype, methodName) {
432
+ const owners = [];
433
+ for(let current = prototype; current && current !== Object.prototype; current = Object.getPrototypeOf(current)){
434
+ if (!Object.prototype.hasOwnProperty.call(current, methodName)) continue;
435
+ if (!Reflect.getOwnMetadata(controller_decorator.META.inheritedFrom, current, methodName)) owners.push(current);
436
+ }
437
+ return owners.reverse();
438
+ }
439
+ /** The entries a method-level stage lists under `key` on each of a handler's {@link stageOwners}, base first. */ function methodStages(prototype, methodName, key) {
440
+ return stageOwners(prototype, methodName).flatMap((owner)=>Reflect.getOwnMetadata(key, owner, methodName) ?? []);
441
+ }
442
+ /** The value a method-level stage holds under `key` on the nearest of a handler's {@link stageOwners} that declares one. */ function nearestMethodStage(prototype, methodName, key) {
443
+ for (const owner of stageOwners(prototype, methodName).reverse()){
444
+ const value = Reflect.getOwnMetadata(key, owner, methodName);
445
+ if (value !== undefined) return value;
446
+ }
447
+ return undefined;
448
+ }
429
449
  /**
430
450
  * Caches a function of a class's prototype and one of its handlers. Handler metadata is fixed once
431
451
  * the classes are decorated, so dispatch resolves each handler's stages once, not on every call.
@@ -477,50 +497,165 @@ const warnedShared = new WeakMap();
477
497
  * The guards dispatch runs before a handler: its classes' class-level guards, outermost first, then
478
498
  * the method's. A method that is no handler has only its own, since class guards apply to handlers.
479
499
  */ const handlerGuards = perHandler((prototype, methodName)=>{
480
- const source = sourcePrototype(prototype, methodName);
481
- if (!source) return [];
500
+ if (!sourcePrototype(prototype, methodName)) return [];
482
501
  return [
483
502
  ...handlerMethods$1(prototype).has(methodName) ? stageClasses(prototype, methodName).flatMap(classLevelGuards) : [],
484
- ...Reflect.getOwnMetadata(controller_decorator.META.methodGuards, source, methodName) ?? []
503
+ ...methodStages(prototype, methodName, controller_decorator.META.methodGuards)
485
504
  ];
486
505
  });
487
- const wrapperCount = perHandler((prototype, methodName)=>{
488
- const owner = declaringPrototype(prototype, methodName);
489
- return owner ? Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, owner, methodName) ?? 0 : 0;
506
+ /**
507
+ * The type a direct call to a handler runs as, which is the one dispatch gives it: `'event'` for an `@On` or `@Once`
508
+ * handler, as its first argument may be a message or an interaction; otherwise read from the first argument.
509
+ */ const directCallType = perHandler((prototype, methodName)=>{
510
+ if (!getEventHandlers(prototype).some((handler)=>handler.method === methodName)) return undefined;
511
+ const routed = [
512
+ ...Object.values(controller_decorator.getCommandMap(prototype) ?? {}).flat().map((command)=>command.methodName),
513
+ ...controller_decorator.getMessageHandlers(prototype).map((handler)=>handler.method),
514
+ ...controller_decorator.getReactionHandlers(prototype).map((handler)=>handler.method),
515
+ ...controller_decorator.getAutocompleteHandlers(prototype).map((handler)=>handler.methodName)
516
+ ];
517
+ // A method dispatch also routes as a command, message or reaction has no one type
518
+ return routed.includes(methodName) ? undefined : 'event';
519
+ });
520
+ /** Whether `@UseGuard` wraps the method anywhere on the chain, an override's base included. */ const isWrapped = perHandler((prototype, methodName)=>{
521
+ for(let current = prototype; current; current = Object.getPrototypeOf(current)){
522
+ if (Object.prototype.hasOwnProperty.call(current, methodName) && Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, current, methodName)) return true;
523
+ }
524
+ return false;
490
525
  });
491
526
  /**
492
- * Calls dispatch has already guarded, keyed by first argument and method, counting the wrappers still
493
- * to pass. A user decorator that awaits between two wrappers leaves a pass pending during that await,
494
- * which a concurrent direct call with the same object and method could take; other overlaps fail closed.
495
- * An event whose first argument is not an object, such as `debug`'s string, is keyed by the instance.
496
- */ const dispatched = new WeakMap();
497
- /** The object a call's dispatch mark is kept on: its first argument, or the instance when that is not an object. */ function markKey(first, instance) {
498
- return typeof first === 'object' && first !== null ? first : instance;
499
- }
500
- /** Takes one pass for the wrapper being entered, if dispatch left one for this call. */ function consumeDispatchMark(first, instance, methodName) {
501
- const marks = dispatched.get(markKey(first, instance));
502
- const remaining = marks?.get(methodName);
503
- if (!marks || !remaining) return false;
504
- if (remaining > 1) marks.set(methodName, remaining - 1);
505
- else marks.delete(methodName);
527
+ * The pass of the guarded call running. Kept per call, so a concurrent call with the same object and method runs its own
528
+ * guards, and closed when the call returns, so work it leaves running, such as a timer, does not carry it.
529
+ */ const guardedPasses = new node_async_hooks.AsyncLocalStorage();
530
+ /** Takes the pass for `wrapper` when it belongs to a call whose guards have already run and has not let it through yet. */ function takeGuardedPass(wrapper, first, instance, methodName) {
531
+ const pass = guardedPasses.getStore();
532
+ if (!pass?.open || pass.instance !== instance || pass.methodName !== methodName || pass.first !== first) return false;
533
+ if (pass.entered.has(wrapper)) return false;
534
+ pass.entered.add(wrapper);
506
535
  return true;
507
536
  }
537
+ /** Runs a call whose guards have run, with its pass; `wrapper` is the one that ran them, which it has entered. */ async function runWithPass(instance, methodName, args, call, wrapper) {
538
+ const pass = {
539
+ instance,
540
+ methodName,
541
+ first: args[0],
542
+ entered: new Set(wrapper ? [
543
+ wrapper
544
+ ] : []),
545
+ open: true
546
+ };
547
+ try {
548
+ return await guardedPasses.run(pass, call);
549
+ } finally{
550
+ pass.open = false;
551
+ }
552
+ }
553
+ /**
554
+ * Adds guards to a method's class or method list, then republishes the effective list under
555
+ * `META.guards`, in the order they run: the bases' before the class's own, and within one class or method the
556
+ * latest decorator first, since it wraps outermost.
557
+ */ function recordGuards(key, guards, prototype, methodName) {
558
+ const existing = Reflect.getOwnMetadata(key, prototype, methodName) ?? [];
559
+ const bases = key === controller_decorator.META.classGuards ? Reflect.getOwnMetadata(controller_decorator.META.baseClassGuards, prototype, methodName) ?? 0 : 0;
560
+ Reflect.defineMetadata(key, [
561
+ ...existing.slice(0, bases),
562
+ ...guards,
563
+ ...existing.slice(bases)
564
+ ], prototype, methodName);
565
+ const [classGuards, inheritedGuards] = [
566
+ controller_decorator.META.classGuards,
567
+ controller_decorator.META.inheritedGuards
568
+ ].map((list)=>Reflect.getOwnMetadata(list, prototype, methodName) ?? []);
569
+ // The method's guards and those of the methods it overrides, base first, as dispatch runs them
570
+ const methodGuards = methodStages(prototype, methodName, controller_decorator.META.methodGuards);
571
+ Reflect.defineMetadata(controller_decorator.META.guards, [
572
+ ...inheritedGuards,
573
+ ...classGuards,
574
+ ...methodGuards
575
+ ], prototype, methodName);
576
+ }
577
+ /** The classes {@link guardOwnHandlersWithBaseGuards} has handled, as `@Controller` and each app's startup both call it. */ const guardedWithBases = new WeakSet();
578
+ /**
579
+ * Gives each handler a class declares itself the class-level guards of the classes it extends,
580
+ * as its inherited handlers have: recorded under `META.guards`, and wrapped so a direct call
581
+ * runs them when no other guard wraps the handler. `@Controller` calls this as it decorates, and
582
+ * {@link prepareDirectCalls} for every class an app runs, a service included. Once per class.
583
+ */ function guardOwnHandlersWithBaseGuards(target) {
584
+ if (guardedWithBases.has(target)) return;
585
+ guardedWithBases.add(target);
586
+ const prototype = target.prototype;
587
+ for (const methodName of handlerMethods$1(prototype)){
588
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
589
+ // Inherited handlers have the chain of the class that declares them
590
+ if (typeof descriptor?.value !== 'function' || Reflect.getOwnMetadata(controller_decorator.META.inheritedFrom, prototype, methodName)) continue;
591
+ // Base first, as dispatch runs them
592
+ const guards = classChain(prototype, prototype).slice(1).reverse().flatMap(classLevelGuards);
593
+ if (guards.length === 0) continue;
594
+ if (!Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, prototype, methodName)) {
595
+ applyGuards(descriptor, prototype, methodName);
596
+ Object.defineProperty(prototype, methodName, descriptor);
597
+ }
598
+ recordGuards(controller_decorator.META.inheritedGuards, guards, prototype, methodName);
599
+ }
600
+ }
508
601
  /** The container that made each instance, so a direct call runs in its own app or testing module. */ const instanceContainers = new WeakMap();
509
602
  /**
510
- * Records each instance `container` makes of `classes` as its own. Two testing modules made from one class each run a
511
- * direct call on their own instance in themselves, where the class's metadata names only the last.
512
- */ function recordInstanceContainers(container, classes) {
603
+ * Readies direct calls to the handlers of the classes an app or testing module runs, controllers and services alike:
604
+ * - stamps each class with the container, and records each instance the container makes as its own, so two testing
605
+ * modules made from one class each run a direct call on their own instance in themselves;
606
+ * - gives each handler a class declares itself the class-level guards of the classes it extends, and wraps each
607
+ * override of a guarded handler, so a direct call to it runs the guards it inherits before its body.
608
+ */ function prepareDirectCalls(container, classes) {
513
609
  for (const cls of classes){
610
+ Reflect.defineMetadata(controller_decorator.META.container, container, cls);
611
+ // Each class on the chain, base first, so an override above the one the app runs is wrapped too
612
+ for (const owner of classesOf(cls).reverse())guardOwnHandlersWithBaseGuards(owner);
613
+ wrapGuardedOverrides(cls.prototype);
514
614
  container.onActivation(cls, (_context, instance)=>{
515
615
  if (typeof instance === 'object' && instance !== null) instanceContainers.set(instance, container);
516
616
  return instance;
517
617
  });
518
618
  }
519
619
  }
620
+ /** `cls` and the classes it extends, innermost first. */ function classesOf(cls) {
621
+ const classes = [];
622
+ for(let current = cls.prototype; current && current !== Object.prototype; current = Object.getPrototypeOf(current)){
623
+ classes.push(current.constructor);
624
+ }
625
+ return classes;
626
+ }
627
+ /** Wraps each method on the chain that overrides a handler a base method guards and is not wrapped itself. */ function wrapGuardedOverrides(prototype) {
628
+ for (const methodName of handlerMethods$1(prototype)){
629
+ let guardedBelow = false;
630
+ for (const owner of stageOwners(prototype, methodName)){
631
+ const descriptor = Object.getOwnPropertyDescriptor(owner, methodName);
632
+ if (guardedBelow && typeof descriptor?.value === 'function' && !Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, owner, methodName)) {
633
+ applyGuards(descriptor, owner, methodName);
634
+ Object.defineProperty(owner, methodName, descriptor);
635
+ }
636
+ if ((Reflect.getOwnMetadata(controller_decorator.META.methodGuards, owner, methodName) ?? []).length > 0) guardedBelow = true;
637
+ }
638
+ }
639
+ }
640
+ /**
641
+ * Wraps a method so a direct call runs its guards first: the wrapper entered first runs the whole
642
+ * chain dispatch resolves for the handler, and lets the ones inside it through. A call from dispatch,
643
+ * which has already run the guards, passes through every wrapper.
644
+ */ function applyGuards(descriptor, prototype, propertyKey) {
645
+ const originalMethod = descriptor.value;
646
+ // Which wrapper a call has passed, so it passes each once
647
+ const wrapper = {};
648
+ descriptor.value = controller_decorator.wrapsMethod(async function(...args) {
649
+ if (takeGuardedPass(wrapper, args[0], this, propertyKey)) return originalMethod.apply(this, args);
650
+ return runDirectCall(this, propertyKey, args, ()=>originalMethod.apply(this, args), wrapper);
651
+ }, originalMethod);
652
+ const wrappers = Reflect.getOwnMetadata(controller_decorator.META.guardWrappers, prototype, propertyKey) ?? 0;
653
+ Reflect.defineMetadata(controller_decorator.META.guardWrappers, wrappers + 1, prototype, propertyKey);
654
+ }
520
655
  /**
521
- * Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves them, then the
522
- * call with a pass for each wrapper inside the one entered, so no guard runs twice.
523
- */ async function runDirectCall(instance, methodName, args, call) {
656
+ * Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves and types them, then
657
+ * the call with a pass every wrapper inside the one entered takes, so no guard runs twice.
658
+ */ async function runDirectCall(instance, methodName, args, call, wrapper) {
524
659
  const prototype = Object.getPrototypeOf(instance);
525
660
  const controller = instance.constructor;
526
661
  // The container that made the instance, or else the app that made the class or a class it extends: a subclass the
@@ -529,48 +664,30 @@ const wrapperCount = perHandler((prototype, methodName)=>{
529
664
  if (!container) {
530
665
  throw new Error(`${controller.name}.${methodName}: @UseGuard runs its guards through the app that made ${controller.name}, and this ` + `instance was made outside one. Inject ${controller.name}, or call it on an instance the app made.`);
531
666
  }
667
+ const type = directCallType(prototype, methodName);
532
668
  if (!await runGuards(handlerGuards(prototype, methodName), {
533
669
  container,
534
670
  controller,
535
671
  methodName,
536
- args
672
+ args,
673
+ type
537
674
  })) return undefined;
538
- const inner = wrapperCount(prototype, methodName) - 1;
539
- if (inner <= 0) return call();
540
- const key = markKey(args[0], instance);
541
- let marks = dispatched.get(key);
542
- if (!marks) dispatched.set(key, marks = new Map());
543
- marks.set(methodName, inner);
544
- try {
545
- return await call();
546
- } finally{
547
- marks.delete(methodName);
548
- }
675
+ return runWithPass(instance, methodName, args, call, wrapper);
549
676
  }
550
677
  /**
551
- * Calls a handler dispatch has already guarded. Its wrappers let this one call through; the passes
552
- * are removed afterwards, so a later direct call with the same (cached) object runs its guards.
678
+ * Calls a handler dispatch has already guarded. Its wrappers let this one call through; the pass
679
+ * closes afterwards, so a later direct call with the same (cached) object runs its guards.
553
680
  */ async function callGuardedHandler(instance, methodName, args) {
554
- const count = wrapperCount(Object.getPrototypeOf(instance), methodName);
555
- if (count === 0) return instance[methodName](...args);
556
- const key = markKey(args[0], instance);
557
- let marks = dispatched.get(key);
558
- if (!marks) dispatched.set(key, marks = new Map());
559
- marks.set(methodName, count);
560
- try {
561
- return await instance[methodName](...args);
562
- } finally{
563
- marks.delete(methodName);
564
- }
681
+ if (!isWrapped(Object.getPrototypeOf(instance), methodName)) return instance[methodName](...args);
682
+ return runWithPass(instance, methodName, args, ()=>instance[methodName](...args));
565
683
  }
566
684
 
567
- /** The cooldowns on a handler: each class's, base first, then the method's. */ const handlerCooldowns = perHandler((prototype, methodName)=>{
568
- const source = sourcePrototype(prototype, methodName);
569
- if (!source) return [];
685
+ /** The cooldowns on a handler: each class's, base first, then the method's and its overrides', base first. */ const handlerCooldowns = perHandler((prototype, methodName)=>{
686
+ if (!sourcePrototype(prototype, methodName)) return [];
570
687
  return [
571
688
  // Base first, as guards run
572
689
  ...stageClasses(prototype, methodName).flatMap((cls)=>Reflect.getOwnMetadata(controller_decorator.META.classCooldowns, cls) ?? []),
573
- ...Reflect.getOwnMetadata(controller_decorator.META.methodCooldowns, source, methodName) ?? []
690
+ ...methodCooldowns(prototype, methodName)
574
691
  ];
575
692
  });
576
693
  /** The counted cooldowns of each list {@link handlerCooldowns} gives, which is the same for a handler. */ const countedCooldowns = new WeakMap();
@@ -611,9 +728,8 @@ const wrapperCount = perHandler((prototype, methodName)=>{
611
728
  }
612
729
  return counted;
613
730
  }
614
- /** The cooldowns declared on a method itself. */ function methodCooldowns(prototype, methodName) {
615
- const source = sourcePrototype(prototype, methodName);
616
- return source ? Reflect.getOwnMetadata(controller_decorator.META.methodCooldowns, source, methodName) ?? [] : [];
731
+ /** The cooldowns declared on a method itself and on the methods it overrides, base first. */ function methodCooldowns(prototype, methodName) {
732
+ return methodStages(prototype, methodName, controller_decorator.META.methodCooldowns);
617
733
  }
618
734
  /** Who and where a call came from: an interaction's or a message's user, server and channel. */ function caller(first) {
619
735
  if (first instanceof discord_js.BaseInteraction) return {
@@ -650,7 +766,7 @@ const DEFAULT_POLICY = {
650
766
  /** The app's cooldown policy, else refusing calls after a second without an answer. */ function cooldownPolicyOf(container) {
651
767
  return container.isBound(COOLDOWN_POLICY) ? container.get(COOLDOWN_POLICY) : DEFAULT_POLICY;
652
768
  }
653
- const logger$3 = new controller_decorator.Logger('Cooldown');
769
+ const logger$4 = new controller_decorator.Logger('Cooldown');
654
770
  /**
655
771
  * How long after its last failure a store must answer for its outage to end. A store that fails some calls and answers
656
772
  * others, as a cluster with one node down does, stays in one outage rather than starting a new one with each failure.
@@ -671,15 +787,15 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
671
787
  });
672
788
  const name = store.constructor.name;
673
789
  const reason = error.timedOut ? `did not answer within ${timeoutMs} ms` : `failed: ${String(error.cause?.message ?? error.cause)}`;
674
- if (failure === 'allow') logger$3.warn(`The cooldown store ${name} ${reason}. Calls run uncounted until it answers again.`);
675
- else logger$3.error(`The cooldown store ${name} ${reason}. Calls with a cooldown are refused until it answers again.`, error.cause ?? '');
790
+ if (failure === 'allow') logger$4.warn(`The cooldown store ${name} ${reason}. Calls run uncounted until it answers again.`);
791
+ else logger$4.error(`The cooldown store ${name} ${reason}. Calls with a cooldown are refused until it answers again.`, error.cause ?? '');
676
792
  }
677
793
  /** Logs that a store answers again, once it has gone {@link OUTAGE_QUIET_MS} without a failure. */ function reportRecovery(store) {
678
794
  const outage = outages.get(store);
679
795
  if (!outage || Date.now() - outage.last < OUTAGE_QUIET_MS) return;
680
796
  outages.delete(store);
681
797
  const seconds = Math.round((outage.last - outage.since) / 1000);
682
- logger$3.log(`The cooldown store ${store.constructor.name} answers again, after ${outage.failures} failed call(s) over ${seconds}s.`);
798
+ logger$4.log(`The cooldown store ${store.constructor.name} answers again, after ${outage.failures} failed call(s) over ${seconds}s.`);
683
799
  }
684
800
  /** What a container's calls wait for before asking its store: the store's own `onReady`, which may connect it. */ const storesReady = new WeakMap();
685
801
  /** The store operations a container's calls have under way, which its store's `onShutdown` waits for. */ const storeOperations = new WeakMap();
@@ -741,7 +857,7 @@ const logger$3 = new controller_decorator.Logger('Cooldown');
741
857
  } catch (error) {
742
858
  if (error instanceof controller_decorator.CooldownStoreError && error.timedOut && !peek && !keepLate) {
743
859
  // Under way from now, so the store does not shut down before the call it counted is given back
744
- const release = attempt.then((verdict)=>verdict.allowed ? verdict.release?.() : undefined).catch((failure)=>logger$3.debug(`Could not release a call the cooldown store counted too late: ${String(failure)}`));
860
+ const release = attempt.then((verdict)=>verdict.allowed ? verdict.release?.() : undefined).catch((failure)=>logger$4.debug(`Could not release a call the cooldown store counted too late: ${String(failure)}`));
745
861
  trackStoreOperation(container, release);
746
862
  }
747
863
  throw error instanceof controller_decorator.CooldownStoreError ? error : new controller_decorator.CooldownStoreError(error, false);
@@ -884,7 +1000,7 @@ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[
884
1000
  try {
885
1001
  return (await askWithin(container, cooldownStoreOf(container), notices, cooldownPolicyOf(container).timeoutMs, false)).allowed;
886
1002
  } catch (error) {
887
- logger$3.debug(`Could not ask the cooldown store whether to tell a refused caller: ${String(error.cause ?? error)}`);
1003
+ logger$4.debug(`Could not ask the cooldown store whether to tell a refused caller: ${String(error.cause ?? error)}`);
888
1004
  return false;
889
1005
  }
890
1006
  }
@@ -1058,6 +1174,7 @@ function describeLength(value) {
1058
1174
  if (existing === undefined) {
1059
1175
  const name = typeof body.name === 'string' ? body.name : commandName;
1060
1176
  unlocalizable.push(...localizationProblems(name, body));
1177
+ const builderName = builderClass?.name;
1061
1178
  byKey.set(key, {
1062
1179
  builderClass: builderClass ?? builder,
1063
1180
  command: {
@@ -1065,6 +1182,9 @@ function describeLength(value) {
1065
1182
  body,
1066
1183
  ...guilds && {
1067
1184
  guilds
1185
+ },
1186
+ ...typeof builderName === 'string' && builderName && {
1187
+ builder: builderName
1068
1188
  }
1069
1189
  }
1070
1190
  });
@@ -1102,6 +1222,14 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
1102
1222
  }
1103
1223
  /** Whether a command is an activity's primary entry point, which Discord takes only globally. */ const isEntryPoint = (command)=>command.body.type === discord_js.ApplicationCommandType.PrimaryEntryPoint;
1104
1224
  /** What a blank guild to register every command to says, as an unset variable gives one. */ const BLANK_GUILD = 'No commands were registered: the guild to register to is blank. Give a guild id, or leave it out to register where commands.* says.';
1225
+ /**
1226
+ * Names every command whose builder lists guilds but no guild id, each with its builder, and says the run registers
1227
+ * none of them yet still succeeds.
1228
+ */ function blankGuildsWarning(commands) {
1229
+ const one = commands.length === 1;
1230
+ const lines = commands.map((command)=>` ${command.builder ? `${command.builder}: ` : ''}"${command.name}"`);
1231
+ return `${commands.length} ${one ? 'command is' : 'commands are'} not registered: ${one ? 'its builder lists' : 'their builders list'} ` + 'no guild ids, as when the variable holding them is unset.\n' + `${lines.join('\n')}\n` + 'Set the guild ids; a builder without guilds registers where commands.* says. `meocord register` still exits 0 for ' + 'these; the next major version (5.0) exits 1.';
1232
+ }
1105
1233
  /**
1106
1234
  * Where each command is sent: one bulk update per scope. A scope's update replaces everything the application has
1107
1235
  * there, so the default scope is included even when it ends up empty, unless `commands.guilds` lists no guild id.
@@ -1145,6 +1273,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
1145
1273
  guild
1146
1274
  });
1147
1275
  const unregistered = [];
1276
+ const blankGuilds = [];
1148
1277
  for (const command of commands){
1149
1278
  if (isEntryPoint(command)) {
1150
1279
  if (command.guilds) {
@@ -1154,7 +1283,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
1154
1283
  } else if (command.guilds) {
1155
1284
  const own = ids(command.guilds);
1156
1285
  // Falling back to the default scope could publish a staff-only command everywhere.
1157
- if (own.length === 0) logger.warn(`Command "${command.name}" lists no guild ids in its builder, so it is not registered.`);
1286
+ if (own.length === 0) blankGuilds.push(command);
1158
1287
  for (const guild of own)add({
1159
1288
  guild
1160
1289
  }, command);
@@ -1168,6 +1297,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
1168
1297
  }, command);
1169
1298
  }
1170
1299
  }
1300
+ if (blankGuilds.length > 0) logger.warn(blankGuildsWarning(blankGuilds));
1171
1301
  if (nowhere) {
1172
1302
  const outcome = unregistered.length === 0 ? targets.has('global') ? 'only the entry point command is registered globally' : 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
1173
1303
  logger.warn(`commands.guilds lists no guild id, as an unset environment variable leaves it, so ${outcome}. ` + `Set the guild ids, or remove commands.guilds to register globally.`);
@@ -1378,10 +1508,7 @@ function logRegistered(target, logger) {
1378
1508
 
1379
1509
  /** How long `@Defer({ mode: 'auto' })` waits for an answer by default. */ const AUTO_DEFER_AFTER_MS = 1500;
1380
1510
  /** The latest `'auto'` acknowledges, counted from the interaction's creation: Discord allows three seconds. */ const AUTO_DEFER_LIMIT_MS = 2500;
1381
- /** The `@Defer` options of a handler, read where the handler is declared. */ const handlerDefer = perHandler((prototype, methodName)=>{
1382
- const source = sourcePrototype(prototype, methodName);
1383
- return source ? Reflect.getOwnMetadata(controller_decorator.META.deferOptions, source, methodName) : undefined;
1384
- });
1511
+ /** The `@Defer` options of a handler: its own, or else those of the nearest method it overrides that has them. */ const handlerDefer = perHandler((prototype, methodName)=>nearestMethodStage(prototype, methodName, controller_decorator.META.deferOptions));
1385
1512
  /** The kind of non-interaction handler a method is, if any: `@Defer` cannot apply to one. */ function nonInteractionHandler(prototype, methodName) {
1386
1513
  if (controller_decorator.getMessageHandlers(prototype).some((handler)=>handler.method === methodName)) return 'message';
1387
1514
  if (controller_decorator.getReactionHandlers(prototype).some((handler)=>handler.method === methodName)) return 'reaction';
@@ -1438,21 +1565,8 @@ function logRegistered(target, logger) {
1438
1565
  */ function isAppClassToken(token) {
1439
1566
  return typeof token === 'function' && token !== discord_js.Client && !NATIVE_SOURCE$1.test(Function.prototype.toString.call(token));
1440
1567
  }
1441
- /**
1442
- * The bound classes `cls` injects, by constructor type or `@inject` token: the classes whose hooks
1443
- * run before its own.
1444
- */ function lifecycleDependencies(container, cls) {
1445
- return injectedTokens(cls).filter((token)=>isAppClassToken(token) && container.isBound(token));
1446
- }
1447
- /** The units for a list of classes already in dependency order, as the app runs them without providers. */ function classUnits(container, classes) {
1448
- return classes.map((cls)=>({
1449
- token: cls,
1450
- name: cls.name,
1451
- dependencies: lifecycleDependencies(container, cls)
1452
- }));
1453
- }
1454
1568
 
1455
- const logger$2 = new controller_decorator.Logger('Interceptor');
1569
+ const logger$3 = new controller_decorator.Logger('Interceptor');
1456
1570
  /** A function's source when the engine wrote it, as for a bound function, a built-in, or what `await` hands a promise. */ const NATIVE_SOURCE = /\{\s*\[native code\]\s*\}$/;
1457
1571
  /**
1458
1572
  * Whether `onRejected` is the app's own handling of a rejection, rather than an engine's step passing it on. The
@@ -1468,13 +1582,12 @@ function isInterceptorWithParams(entry) {
1468
1582
  }
1469
1583
  /**
1470
1584
  * The interceptors around a handler: each class's, base first, down to the controller it is dispatched on, then the
1471
- * method's.
1585
+ * method's and its overrides', base first.
1472
1586
  */ const handlerInterceptors = perHandler((prototype, methodName)=>{
1473
- const source = sourcePrototype(prototype, methodName);
1474
- if (!source) return [];
1587
+ if (!sourcePrototype(prototype, methodName)) return [];
1475
1588
  return [
1476
1589
  ...stageClasses(prototype, methodName).flatMap((cls)=>Reflect.getOwnMetadata(controller_decorator.META.classInterceptors, cls) ?? []),
1477
- ...Reflect.getOwnMetadata(controller_decorator.META.methodInterceptors, source, methodName) ?? []
1590
+ ...methodStages(prototype, methodName, controller_decorator.META.methodInterceptors)
1478
1591
  ];
1479
1592
  });
1480
1593
  /** Binds `cls` and its unbound dependencies as singletons, refusing any that injects `ExecutionContext`. */ function bindShared(container, cls) {
@@ -1623,7 +1736,7 @@ function isInterceptorWithParams(entry) {
1623
1736
  // Once the handlers chained from it have had their turn, and those the app's code returned a promise from settled
1624
1737
  void taken.settled.then(()=>new Promise((resolve)=>setTimeout(resolve, 0))).then(()=>Promise.all(taken.disposing)).then(()=>{
1625
1738
  if (!taken.failure || taken.caught) return;
1626
- logger$2.warn(`${cls.name} returned before ${context.getController().name}.${context.getHandlerName()} finished, which then threw; ` + 'nothing caught it, so the call could not report it:', taken.failure.error);
1739
+ logger$3.warn(`${cls.name} returned before ${context.getController().name}.${context.getHandlerName()} finished, which then threw; ` + 'nothing caught it, so the call could not report it:', taken.failure.error);
1627
1740
  });
1628
1741
  }
1629
1742
  // Only a run left on a chain that ends unhandled is the call's; one the interceptor took on, as a timeout
@@ -1659,13 +1772,13 @@ function isInterceptorWithParams(entry) {
1659
1772
  ];
1660
1773
  }
1661
1774
  /** A handler's method and class filters, the classes innermost first, resolved once. */ const ownFilterLevels = perHandler((prototype, methodName)=>{
1662
- const source = sourcePrototype(prototype, methodName);
1663
- if (!source) return {
1775
+ if (!sourcePrototype(prototype, methodName)) return {
1664
1776
  method: [],
1665
1777
  classes: []
1666
1778
  };
1667
1779
  return {
1668
- method: Reflect.getOwnMetadata(controller_decorator.META.methodFilters, source, methodName) ?? [],
1780
+ // An override's filters before those of the methods it overrides, innermost first as the classes are
1781
+ method: stageOwners(prototype, methodName).reverse().flatMap((owner)=>Reflect.getOwnMetadata(controller_decorator.META.methodFilters, owner, methodName) ?? []),
1669
1782
  classes: [
1670
1783
  ...stageClasses(prototype, methodName)
1671
1784
  ].reverse().flatMap((cls)=>Reflect.getOwnMetadata(controller_decorator.META.classFilters, cls) ?? [])
@@ -1684,7 +1797,7 @@ function isInterceptorWithParams(entry) {
1684
1797
  // Every filter reaching here was checked for @Catch at startup, so its types are always recorded
1685
1798
  const types = Reflect.getOwnMetadata(controller_decorator.META.catchTypes, filterClass(entry));
1686
1799
  // Only classes match: anything else given to @Catch, warned of as it applied, matches no error
1687
- if (types.length === 0 || types.some((type)=>controller_decorator.isConstructor(type) && error instanceof type)) return entry;
1800
+ if (types.length === 0 || types.some((type)=>controller_decorator.isConstructible(type) && error instanceof type)) return entry;
1688
1801
  }
1689
1802
  }
1690
1803
  return undefined;
@@ -1709,17 +1822,16 @@ function isPipeWithParams(entry) {
1709
1822
  const asList = (entries)=>Array.isArray(entries) ? entries : [
1710
1823
  entries
1711
1824
  ];
1712
- /** The validation and pipes that run on a handler's input: `@Validate`'s first, then `@UsePipe`'s. */ const handlerInputStages = perHandler((prototype, methodName)=>{
1713
- const source = sourcePrototype(prototype, methodName);
1714
- if (!source) return {
1715
- pipes: []
1716
- };
1717
- const validation = Reflect.getOwnMetadata(controller_decorator.META.methodValidation, source, methodName);
1825
+ /**
1826
+ * The validation and pipes that run on a handler's input: `@Validate`'s first, then `@UsePipe`'s. The `@Validate` is the
1827
+ * handler's own, or else that of the nearest method it overrides that has one; `@UsePipe`'s are every one's, base first.
1828
+ */ const handlerInputStages = perHandler((prototype, methodName)=>{
1829
+ const validation = nearestMethodStage(prototype, methodName, controller_decorator.META.methodValidation);
1718
1830
  const inline = Object.entries(validation?.pipes ?? {}).flatMap(([key, entries])=>asList(entries).map((entry)=>({
1719
1831
  key,
1720
1832
  entry
1721
1833
  })));
1722
- const used = Reflect.getOwnMetadata(controller_decorator.META.methodPipes, source, methodName) ?? [];
1834
+ const used = methodStages(prototype, methodName, controller_decorator.META.methodPipes);
1723
1835
  return {
1724
1836
  schema: validation?.schema,
1725
1837
  pipes: [
@@ -1785,11 +1897,11 @@ const asList = (entries)=>Array.isArray(entries) ? entries : [
1785
1897
  }
1786
1898
 
1787
1899
  /** Where a container keeps the observers its calls are reported to, in order. */ const OBSERVERS = Symbol('observers');
1788
- const logger$1 = new controller_decorator.Logger('Observer');
1900
+ const logger$2 = new controller_decorator.Logger('Observer');
1789
1901
  /** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
1790
1902
  for (const observer of observers){
1791
1903
  if (typeof observer !== 'function' || !Reflect.getMetadata(controller_decorator.META.observerClass, observer)) {
1792
- const name = typeof observer === 'function' ? observer.name : String(observer);
1904
+ const name = controller_decorator.isConstructor(observer) ? observer.name || 'an anonymous class' : typeof observer === 'function' ? 'a function' : String(observer);
1793
1905
  controller_decorator.startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
1794
1906
  continue;
1795
1907
  }
@@ -1831,7 +1943,7 @@ function observersOf(container) {
1831
1943
  */ function notifyStart(container, context) {
1832
1944
  const pending = [];
1833
1945
  for (const observer of observersFor(container, context)){
1834
- const report = (error)=>logger$1.error(`Observer ${observer.name} threw while starting ${describeCall(context)}:`, error);
1946
+ const report = (error)=>logger$2.error(`Observer ${observer.name} threw while starting ${describeCall(context)}:`, error);
1835
1947
  try {
1836
1948
  const instance = container.get(observer);
1837
1949
  if (typeof instance.onStart !== 'function') continue;
@@ -1845,8 +1957,9 @@ function observersOf(container) {
1845
1957
  }
1846
1958
  /**
1847
1959
  * Where an interaction's answer stands: a repliable one's `ResponseState`, and an autocomplete answered
1848
- * or not. `undefined` for anything that is not an interaction.
1960
+ * or not. `undefined` for anything that is not an interaction's call, an event carrying one included.
1849
1961
  */ function responsePhaseOf(context) {
1962
+ if (context.getType() === 'event') return undefined;
1850
1963
  const interaction = context.getInteraction();
1851
1964
  if (!interaction) return undefined;
1852
1965
  if (interaction.isAutocomplete()) return interaction.responded ? 'replied' : 'unanswered';
@@ -1868,13 +1981,14 @@ function observersOf(container) {
1868
1981
  try {
1869
1982
  await container.get(observer).onSettled(context, result);
1870
1983
  } catch (error) {
1871
- logger$1.error(`Observer ${observer.name} threw while observing ${describeCall(context)}:`, error);
1984
+ logger$2.error(`Observer ${observer.name} threw while observing ${describeCall(context)}:`, error);
1872
1985
  }
1873
1986
  }
1874
1987
  }
1875
1988
 
1876
- /** Classes with a `@UseTheme`, on the class or one of its methods. */ const THEMED_CLASSES = new WeakSet();
1877
- const apps = new WeakMap();
1989
+ /** Classes with a `@UseTheme`, on the class or one of its methods, from either build of this version. */ const THEMED_CLASSES = controller_decorator.shared('themedClasses', ()=>new WeakSet());
1990
+ // One per process for this meocord version, so an app started through one build finds its theme from the other
1991
+ const apps = controller_decorator.shared('appThemes', ()=>new WeakMap());
1878
1992
  /** The classes on `cls`'s prototype chain, itself first. */ function chainOf(cls) {
1879
1993
  const chain = [];
1880
1994
  for(let prototype = cls.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
@@ -1905,6 +2019,13 @@ const apps = new WeakMap();
1905
2019
  }
1906
2020
  controller_decorator.attachThemeCache(container.get(controller_decorator.ThemeCache), resolvers);
1907
2021
  }
2022
+ /** Counts `classes`, whose handlers are prepared once the app's providers are made, among those a theme can vary by. */ function prepareLateThemes(container, classes) {
2023
+ const themes = apps.get(container);
2024
+ if (!themes || themes.varies || !classes.some((cls)=>chainOf(cls).some((link)=>THEMED_CLASSES.has(link)))) return;
2025
+ themes.varies = true;
2026
+ // Built again on its next read, with each handler's own theme
2027
+ themes.version = -1;
2028
+ }
1908
2029
  /** The app's themes, built again when the layers beneath the app's have changed. */ function current(container) {
1909
2030
  const themes = apps.get(container);
1910
2031
  if (themes && themes.version !== controller_decorator.themeLayersVersion()) {
@@ -1919,13 +2040,12 @@ const apps = new WeakMap();
1919
2040
  }
1920
2041
  /**
1921
2042
  * Decides whether the app's calls need a scope, again whenever the layers beneath it or the app read outside a call
1922
- * change. They do when the handlers can differ in theme. Otherwise a call reads the theme outside a call, so it
1923
- * needs one unless that is its own theme: when the app owns it, or when no app does and the app's theme is MeoCord's
1924
- * defaults.
2043
+ * change. They do when the handlers can differ in theme. Otherwise a call reads the theme outside a call, so it needs
2044
+ * one unless its app owns that theme: another app may claim it while the call runs, and the call keeps the theme it
2045
+ * started with.
1925
2046
  */ function rescope(container, themes) {
1926
2047
  themes.ambientVersion = controller_decorator.ambientThemeVersion();
1927
- const readsOwnTheme = controller_decorator.ownsAmbientTheme(container) || !controller_decorator.hasAmbientTheme() && themes.app === controller_decorator.defaultTheme();
1928
- themes.scoped = themes.varies || !readsOwnTheme ? themes.app : undefined;
2048
+ themes.scoped = themes.varies || !controller_decorator.ownsAmbientTheme(container) ? themes.app : undefined;
1929
2049
  }
1930
2050
  /** The app's theme: MeoCord's defaults and the app's `@MeoCord({ theme })`, or the defaults for a container with none. */ function appTheme(container) {
1931
2051
  return current(container)?.app ?? controller_decorator.defaultTheme();
@@ -1943,8 +2063,10 @@ const apps = new WeakMap();
1943
2063
  for (const cls of stageClasses(prototype, methodName)){
1944
2064
  theme = controller_decorator.mergeTheme(theme, Reflect.getOwnMetadata(controller_decorator.META.classTheme, cls));
1945
2065
  }
1946
- const source = sourcePrototype(prototype, methodName);
1947
- if (source) theme = controller_decorator.mergeTheme(theme, Reflect.getOwnMetadata(controller_decorator.META.methodTheme, source, methodName));
2066
+ // The method's layer over its class's, and an override's over the method it overrides
2067
+ for (const owner of stageOwners(prototype, methodName)){
2068
+ theme = controller_decorator.mergeTheme(theme, Reflect.getOwnMetadata(controller_decorator.META.methodTheme, owner, methodName));
2069
+ }
1948
2070
  byMethod.set(methodName, theme);
1949
2071
  return theme;
1950
2072
  }
@@ -1983,7 +2105,7 @@ const apps = new WeakMap();
1983
2105
  ready
1984
2106
  };
1985
2107
  }
1986
- const clientApps = new WeakMap();
2108
+ const clientApps = controller_decorator.shared('clientThemes', ()=>new WeakMap());
1987
2109
  /** Records which app a client's interactions come to, so an answer outside any call can find its theme. */ function registerClientTheme(client, container) {
1988
2110
  clientApps.set(client, container);
1989
2111
  }
@@ -2154,19 +2276,35 @@ const stagesByGlobals = new WeakMap();
2154
2276
  for (const entry of globals.filters)prepareFilter(container, entry);
2155
2277
  for (const controller of controllers){
2156
2278
  const prototype = controller.prototype;
2157
- for (const method of handlerMethods(prototype)){
2158
- const kind = handlerDefer(prototype, method) ? nonInteractionHandler(prototype, method) : undefined;
2159
- if (kind) {
2160
- controller_decorator.startupError(deferMisuseError(controller.name, method, kind));
2161
- continue;
2162
- }
2163
- for (const entry of handlerInterceptors(prototype, method))prepareInterceptor(container, entry);
2164
- for (const entry of handlerFilterLevels(prototype, method, []).flat())prepareFilter(container, entry);
2165
- for (const { entry } of handlerInputStages(prototype, method).pipes)preparePipe(container, entry);
2166
- }
2279
+ for (const method of handlerMethods(prototype))prepareHandler(container, controller, method);
2167
2280
  assertInputStagesOnInteractions(controller, prototype);
2168
2281
  }
2169
2282
  }
2283
+ /**
2284
+ * Prepares one `@On` or `@Once` handler of a class the app reaches only as something it makes, as
2285
+ * {@link prepareHandlerStages} prepares a controller's.
2286
+ *
2287
+ * @returns The errors a controller's handler would be refused with; none when the handler is ready to run.
2288
+ */ function prepareEventHandler(container, cls, method) {
2289
+ try {
2290
+ controller_decorator.collectStartupErrors(()=>{
2291
+ prepareHandler(container, cls, method);
2292
+ assertInputStages(cls, cls.prototype, method, 'event');
2293
+ });
2294
+ return [];
2295
+ } catch (error) {
2296
+ if (!controller_decorator.isRefusal(error)) throw error;
2297
+ return controller_decorator.startupErrorsOf(error);
2298
+ }
2299
+ }
2300
+ /** Binds what one handler's interceptors, filters and pipes need, refusing `@Defer` where it cannot apply. */ function prepareHandler(container, controller, method) {
2301
+ const prototype = controller.prototype;
2302
+ const kind = handlerDefer(prototype, method) ? nonInteractionHandler(prototype, method) : undefined;
2303
+ if (kind) return controller_decorator.startupError(deferMisuseError(controller.name, method, kind));
2304
+ for (const entry of handlerInterceptors(prototype, method))prepareInterceptor(container, entry);
2305
+ for (const entry of handlerFilterLevels(prototype, method, []).flat())prepareFilter(container, entry);
2306
+ for (const { entry } of handlerInputStages(prototype, method).pipes)preparePipe(container, entry);
2307
+ }
2170
2308
  /** Records the app's theme for the handlers of `controllers`, once the startup checks have passed: it makes the theme cache. */ function prepareThemes(container, controllers) {
2171
2309
  const globals = globalStagesOf(container);
2172
2310
  configureThemes(container, globals.theme, controllers, globals.themeFor);
@@ -2212,17 +2350,17 @@ const stagesByGlobals = new WeakMap();
2212
2350
  'event'
2213
2351
  ])
2214
2352
  ];
2215
- for (const [method, kind] of others){
2216
- const { schema, pipes } = handlerInputStages(prototype, method);
2217
- const handler = kind === 'message' ? 'a message handler without a pattern' : `${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} handler`;
2218
- if (schema || pipes.length > 0) {
2219
- controller_decorator.startupError(new Error(`${controller.name}.${method}: @Validate and @UsePipe are for interaction and patterned message handlers, ` + `whose options, customId params, modal fields and pattern params they check, and this is ${handler}.`));
2220
- continue;
2221
- }
2222
- // A controller's own @Cooldown skips these handlers; one on the method itself is a mistake.
2223
- if (kind !== 'message' && methodCooldowns(prototype, method).length > 0) {
2224
- controller_decorator.startupError(new Error(`${controller.name}.${method}: @Cooldown is for interaction and message handlers, and this is ${handler}.`));
2225
- }
2353
+ for (const [method, kind] of others)assertInputStages(controller, prototype, method, kind);
2354
+ }
2355
+ /** Refuses `@Validate` and `@UsePipe` on one handler of `kind` with no params to check, and `@Cooldown` where it never counts. */ function assertInputStages(controller, prototype, method, kind) {
2356
+ const { schema, pipes } = handlerInputStages(prototype, method);
2357
+ const handler = kind === 'message' ? 'a message handler without a pattern' : `${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} handler`;
2358
+ if (schema || pipes.length > 0) {
2359
+ return controller_decorator.startupError(new Error(`${controller.name}.${method}: @Validate and @UsePipe are for interaction and patterned message handlers, ` + `whose options, customId params, modal fields and pattern params they check, and this is ${handler}.`));
2360
+ }
2361
+ // A controller's own @Cooldown skips these handlers; one on the method itself is a mistake.
2362
+ if (kind !== 'message' && methodCooldowns(prototype, method).length > 0) {
2363
+ controller_decorator.startupError(new Error(`${controller.name}.${method}: @Cooldown is for interaction and message handlers, and this is ${handler}.`));
2226
2364
  }
2227
2365
  }
2228
2366
  /**
@@ -2250,7 +2388,7 @@ const stagesByGlobals = new WeakMap();
2250
2388
  if (response) result.response = response;
2251
2389
  return result;
2252
2390
  }
2253
- const logger = new controller_decorator.Logger('ExceptionFilter');
2391
+ const logger$1 = new controller_decorator.Logger('ExceptionFilter');
2254
2392
  /**
2255
2393
  * Hands a failed call's error to the first matching filter, level by level, then to the fallback. A
2256
2394
  * filter that throws is logged and the fallback answers the original error; without a fallback, the
@@ -2264,7 +2402,7 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
2264
2402
  } catch (filterError) {
2265
2403
  if (!fallback) throw filterError;
2266
2404
  const name = (typeof filter === 'object' ? filter.provide : filter).name;
2267
- logger.error(`Filter ${name} threw while handling an error:`, filterError);
2405
+ logger$1.error(`Filter ${name} threw while handling an error:`, filterError);
2268
2406
  }
2269
2407
  }
2270
2408
  if (!fallback) throw error;
@@ -2332,7 +2470,9 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
2332
2470
  denial
2333
2471
  })) {
2334
2472
  outcome = 'denied';
2335
- await response?.abandon();
2473
+ // Without @Defer too, so a denied component is acknowledged rather than left to fail
2474
+ const denied = response ?? (type === 'interaction' && first?.isRepliable?.() ? controller_decorator.responseOf(first) : undefined);
2475
+ await denied?.abandon();
2336
2476
  return {
2337
2477
  ran: false
2338
2478
  };
@@ -2388,14 +2528,18 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
2388
2528
  };
2389
2529
  } finally{
2390
2530
  await response?.release();
2391
- if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && type === 'interaction') {
2531
+ if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && (type === 'interaction' || type === 'autocomplete')) {
2392
2532
  const phase = responsePhaseOf(contextOf());
2393
2533
  // The handler is to blame only once it has finished; until then, the interceptor that ended the call is
2394
- const blamed = !finished && returnedEarly ? {
2534
+ const returnedBy = !finished && returnedEarly ? {
2395
2535
  interceptor: returnedEarly,
2396
2536
  handlerStarted: ran
2397
2537
  } : undefined;
2398
- if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered(phase, blamed);
2538
+ if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered({
2539
+ type,
2540
+ phase,
2541
+ returnedBy
2542
+ });
2399
2543
  }
2400
2544
  if (options.awaitObservers) await starting;
2401
2545
  // After the answer and the release, so the duration covers the whole call
@@ -2464,6 +2608,23 @@ const logger = new controller_decorator.Logger('ExceptionFilter');
2464
2608
  }, options);
2465
2609
  }
2466
2610
 
2611
+ /** The longest delay a timer keeps: 2^31 - 1 ms, about 24.8 days. Node fires a longer one at once. */ const MAX_TIMER_MS = 2147483647;
2612
+ const MS_PER_SECOND = 1000;
2613
+ /** The most whole seconds a timer keeps, as a message about an option given in seconds names the limit. */ const MAX_TIMER_SECONDS = Math.floor(MAX_TIMER_MS / MS_PER_SECOND);
2614
+ /** Whether a timer keeps a delay given in seconds, a fraction of a second included. */ const timerKeepsSeconds = (seconds)=>seconds * MS_PER_SECOND <= MAX_TIMER_MS;
2615
+ /** A value as a refusal quotes it: a number as itself, `Infinity` and `NaN` included, a string in quotes. */ function quoted(value) {
2616
+ if (typeof value === 'string') return JSON.stringify(value);
2617
+ if (typeof value === 'number' || typeof value === 'boolean' || value === null) return String(value);
2618
+ return Array.isArray(value) ? 'an array' : typeof value;
2619
+ }
2620
+ /**
2621
+ * What is wrong with a timeout option, or `undefined` when it is a number of milliseconds a timer keeps: finite, above
2622
+ * 0, or 0 or more with `allowZero`, and at most `max`, {@link MAX_TIMER_MS} unless less. The caller names the option.
2623
+ */ function timeoutProblem(value, { allowZero = false, max = MAX_TIMER_MS } = {}) {
2624
+ if (typeof value === 'number' && Number.isFinite(value) && (allowZero ? value >= 0 : value > 0) && value <= max) return undefined;
2625
+ return `must be a number of milliseconds ${allowZero ? '0 or more' : 'above 0'}, at most ${max} (got ${quoted(value)})`;
2626
+ }
2627
+
2467
2628
  /** Fetches in flight for a client, by kind, server and ID, so callers asking at once share one request. */ const inFlight = new WeakMap();
2468
2629
  /** The one request for `key`: the one in flight, or a new one from `start`, forgotten once it settles. */ function singleFlight(client, key, start) {
2469
2630
  let requests = inFlight.get(client);
@@ -2675,7 +2836,8 @@ const isFlagNameChar = (code, first)=>code >= 97 && code <= 122 || code >= 65 &&
2675
2836
  }
2676
2837
  flags.push({
2677
2838
  name,
2678
- value
2839
+ value,
2840
+ at: words.length
2679
2841
  });
2680
2842
  cuts.push(start, j);
2681
2843
  i = j;
@@ -2710,6 +2872,33 @@ const isFlagNameChar = (code, first)=>code >= 97 && code <= 122 || code >= 65 &&
2710
2872
  }
2711
2873
  return (rest + text.slice(from)).trimEnd();
2712
2874
  }
2875
+ const caseKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase();
2876
+ /** Whether a flag, given bare in place `at` among the other words, is a pattern's own `--` word `literal`. */ const isLiteralFlag = (flag, literal, at, caseSensitive)=>flag.at === at && flag.value === undefined && caseKey(`--${flag.name}`, caseSensitive) === caseKey(literal, caseSensitive);
2877
+ /**
2878
+ * The flags a pattern's own `--` words take from a message, as indexes into `flags`: where the pattern has such a word,
2879
+ * a flag of that name given bare in that place among the message's other words is the word, not a flag. A word typed in
2880
+ * quotes is matched as any word is, and is taken first.
2881
+ */ function literalFlags(tokens, caseSensitive, words, flags) {
2882
+ const taken = new Set();
2883
+ if (flags.length === 0) return taken;
2884
+ let w = 0;
2885
+ for (const token of tokens){
2886
+ if (!('literal' in token)) {
2887
+ // Literal words come before the optional and rest params, which a pattern ends with
2888
+ if (token.rest || token.optional) break;
2889
+ w++;
2890
+ continue;
2891
+ }
2892
+ if (w < words.length && caseKey(words[w].value, caseSensitive) === caseKey(token.literal, caseSensitive)) {
2893
+ w++;
2894
+ continue;
2895
+ }
2896
+ const index = flags.findIndex((flag, i)=>!taken.has(i) && isLiteralFlag(flag, token.literal, w, caseSensitive));
2897
+ if (index === -1) break;
2898
+ taken.add(index);
2899
+ }
2900
+ return taken;
2901
+ }
2713
2902
 
2714
2903
  /** The ID a mention or a bare snowflake names, for the mention's kind. */ function idOf(word, mention) {
2715
2904
  return (mention.exec(word) ?? /^(\d{17,20})$/.exec(word))?.[1];
@@ -2717,6 +2906,10 @@ const isFlagNameChar = (code, first)=>code >= 97 && code <= 122 || code >= 65 &&
2717
2906
  const USER_MENTION = /^<@!?(\d{17,20})>$/;
2718
2907
  const ROLE_MENTION = /^<@&(\d{17,20})>$/;
2719
2908
  const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2909
+ /** The server's role a word names: its mention or ID, else its name in any case. */ function roleNamed(guild, word) {
2910
+ const id = idOf(word, ROLE_MENTION);
2911
+ return id ? guild.roles.cache.get(id) : guild.roles.cache.find((candidate)=>candidate.name.toLowerCase() === word.toLowerCase());
2912
+ }
2720
2913
  /** The scalar types, each turning a word into its value or `undefined`, and the Discord ones, which resolve below. */ const BUILT_IN_TYPES = controller_decorator.lookupTable({
2721
2914
  string: (word)=>word,
2722
2915
  int: (word)=>controller_decorator.number(word, true),
@@ -2747,7 +2940,18 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2747
2940
  'channel'
2748
2941
  ]);
2749
2942
  /** Whether a param type is found only in a server. */ const isGuildType = (type)=>GUILD_TYPES.has(type);
2750
- /** Whether a route's pattern takes a param found only in a server, so a DM gets its usage instead. */ const needsGuild = (route)=>route.tokens.some((token)=>'param' in token && token.type !== undefined && GUILD_TYPES.has(token.type));
2943
+ /** Whether a route's pattern takes a `member`, `role` or `channel` param, optional or not. */ const takesGuildParam = (route)=>route.tokens.some((token)=>'param' in token && token.type !== undefined && GUILD_TYPES.has(token.type));
2944
+ /**
2945
+ * Whether a route works only in a server, as dispatch decides it: a `member`, `role` or `channel` param, optional or
2946
+ * not, or a required flag of one of those types. An optional flag of those types refuses a DM only when it is given.
2947
+ */ const needsGuild = (route)=>takesGuildParam(route) || route.flags.some((flag)=>!flag.optional && flag.type !== undefined && GUILD_TYPES.has(flag.type));
2948
+ /**
2949
+ * Where a route works: its scope, narrowed to servers by a `member`, `role` or `channel` param, or required flag, as
2950
+ * dispatch narrows it.
2951
+ */ function effectiveScope(route) {
2952
+ if (route.scope !== 'any') return route.scope;
2953
+ return needsGuild(route) ? 'guild' : 'any';
2954
+ }
2751
2955
  /** Whether a pattern's `{name:type}` names a type: built in, words to choose from, or one the app adds. */ function isKnownParamType(type, types) {
2752
2956
  return type in BUILT_IN_TYPES || Boolean(controller_decorator.choicesOf(type)) || appType(types, type) !== undefined;
2753
2957
  }
@@ -2827,14 +3031,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2827
3031
  */ function assertMessageScope(route, message, start) {
2828
3032
  const inGuild = message.guildId !== null && message.guildId !== undefined;
2829
3033
  if (route.scope === 'guild' && !inGuild) {
2830
- throw new controller_decorator.MessageUsageError(usageOf(route, start), [
2831
- controller_decorator.usageIssue({
2832
- key: 'meocord.usage.serverOnly'
2833
- })
2834
- ], {
2835
- serverOnly: true,
2836
- quiet: start === ''
2837
- });
3034
+ throw serverOnly(usageOf(route, start), start === '');
2838
3035
  }
2839
3036
  if (route.scope === 'dm' && inGuild) {
2840
3037
  throw new controller_decorator.MessageUsageError(usageOf(route, start), [
@@ -2847,6 +3044,15 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2847
3044
  });
2848
3045
  }
2849
3046
  }
3047
+ /** The usage error for a message sent outside a server to a command, or with a flag, that only a server has. */ const serverOnly = (usage, quiet)=>new controller_decorator.MessageUsageError(usage, [
3048
+ controller_decorator.usageIssue({
3049
+ key: 'meocord.usage.serverOnly'
3050
+ })
3051
+ ], {
3052
+ serverOnly: true,
3053
+ quiet
3054
+ });
3055
+ /** Whether a message gives one of a route's flags of a type only a server has, such as `--in=#general`. */ const givesGuildFlag = (route, flags)=>flags.some((given)=>route.flags.some((flag)=>flag.type !== undefined && GUILD_TYPES.has(flag.type) && caseKey(flag.flag, route.caseSensitive) === caseKey(given.name, route.caseSensitive)));
2850
3056
  /** Whether a value an app's `parse` returned is a ref to resolve after the guards. */ const isRef = (value)=>typeof value === 'object' && value !== null && 'cached' in value && typeof value.resolve === 'function';
2851
3057
  /**
2852
3058
  * The params a matched route's guards see, read from the words with no request to Discord: typed params and lists as
@@ -2869,18 +3075,20 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2869
3075
  const issues = [];
2870
3076
  const items = [];
2871
3077
  for (const token of route.tokens){
2872
- if (!('param' in token) || token.type === undefined || raw[token.param] === undefined) continue;
3078
+ // Only a word the message gave is typed: an own one, never what Object.prototype has under the param's name
3079
+ const word = 'param' in token ? controller_decorator.ownValue(raw, token.param) : undefined;
3080
+ if (!('param' in token) || token.type === undefined || word === undefined) continue;
2873
3081
  if (!token.rest) {
2874
3082
  items.push({
2875
3083
  key: token.param,
2876
3084
  label: token.param,
2877
3085
  type: token.type,
2878
- word: raw[token.param]
3086
+ word
2879
3087
  });
2880
3088
  continue;
2881
3089
  }
2882
- const words = splitWords(raw[token.param]);
2883
- params[token.param] = new Array(words.length);
3090
+ const words = splitWords(word);
3091
+ controller_decorator.setOwn(params, token.param, new Array(words.length));
2884
3092
  words.forEach(({ value }, index)=>items.push({
2885
3093
  key: token.param,
2886
3094
  label: token.param,
@@ -2890,19 +3098,12 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2890
3098
  }));
2891
3099
  }
2892
3100
  const flagIssues = route.flags.length > 0 ? readFlags(route, message, start, params, items) : [];
3101
+ // readFlags takes out each flag's key, so one left out whose name Object.prototype has reads as left out again
3102
+ if (route.inheritedNames.length > 0) controller_decorator.shadowInherited(params, route.inheritedNames);
2893
3103
  const guild = message.guild;
2894
- if (!guild && (needsGuild(route) || items.some((item)=>GUILD_TYPES.has(item.type)))) {
2895
- throw new controller_decorator.MessageUsageError(usage, [
2896
- controller_decorator.usageIssue({
2897
- key: 'meocord.usage.serverOnly'
2898
- })
2899
- ], {
2900
- serverOnly: true,
2901
- quiet
2902
- });
2903
- }
3104
+ if (!guild && (needsGuild(route) || items.some((item)=>GUILD_TYPES.has(item.type)))) throw serverOnly(usage, quiet);
2904
3105
  const put = (item, value)=>{
2905
- if (item.index === undefined) params[item.key] = value;
3106
+ if (item.index === undefined) controller_decorator.setOwn(params, item.key, value);
2906
3107
  else params[item.key][item.index] = value;
2907
3108
  };
2908
3109
  const refs = [];
@@ -2939,8 +3140,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2939
3140
  }
2940
3141
  } else if (type === 'role') {
2941
3142
  // Roles are cached with the Guilds intent, so a role is known here or not at all
2942
- const id = idOf(word, ROLE_MENTION);
2943
- const role = id ? guild.roles.cache.get(id) : guild.roles.cache.find((candidate)=>candidate.name.toLowerCase() === word.toLowerCase());
3143
+ const role = roleNamed(guild, word);
2944
3144
  if (role) {
2945
3145
  refer(item, 'role', new MessageEntityRef('role', role.id, message.client, guild));
2946
3146
  continue;
@@ -2980,9 +3180,9 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2980
3180
  const params = {
2981
3181
  ...parsed.params
2982
3182
  };
2983
- for (const [key, value] of Object.entries(params))if (Array.isArray(value)) params[key] = [
3183
+ for (const [key, value] of Object.entries(params))if (Array.isArray(value)) controller_decorator.setOwn(params, key, [
2984
3184
  ...value
2985
- ];
3185
+ ]);
2986
3186
  const issues = [];
2987
3187
  for (const slot of refs){
2988
3188
  const value = slot.ref instanceof MessageEntityRef ? found.get(slot.ref) : await slot.ref.resolve();
@@ -2990,7 +3190,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
2990
3190
  issues.push(missingEntity(slot));
2991
3191
  continue;
2992
3192
  }
2993
- if (slot.index === undefined) params[slot.key] = value;
3193
+ if (slot.index === undefined) controller_decorator.setOwn(params, slot.key, value);
2994
3194
  else params[slot.key][slot.index] = value;
2995
3195
  }
2996
3196
  if (issues.length > 0) throw new controller_decorator.MessageUsageError(usage, issues, {
@@ -3056,7 +3256,11 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
3056
3256
  return {
3057
3257
  parseArgs: async (args)=>{
3058
3258
  assertMessageScope(route, message, start);
3059
- if (given !== undefined) throw new controller_decorator.MessageUsageError(usageOf(route, start), missingParams(route, given));
3259
+ if (given !== undefined) {
3260
+ // A DM to a command only a server has is told so before what its words lack, as one whose words fit is
3261
+ if (!message.guild && (needsGuild(route) || givesGuildFlag(route, given.flags))) throw serverOnly(usageOf(route, start), start === '');
3262
+ throw new controller_decorator.MessageUsageError(usageOf(route, start), missingParams(route, given, types, message.guild));
3263
+ }
3060
3264
  if (!hasTypedParams(route)) return args;
3061
3265
  parsed = await parseMessageParams(route, params, message, start, types);
3062
3266
  return [
@@ -3107,7 +3311,11 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
3107
3311
  const given = new Map();
3108
3312
  const unknown = new Set();
3109
3313
  const text = (message.content ?? '').trim();
3110
- for (const flag of splitFlagWords(text.slice(start.length)).flags){
3314
+ const split = splitFlagWords(text.slice(start.length));
3315
+ // A flag the pattern's own `--` word took is that word, and neither one of its flags nor unknown
3316
+ const literal = literalFlags(route.tokens, route.caseSensitive, split.words, split.flags);
3317
+ for (const [index, flag] of split.flags.entries()){
3318
+ if (literal.has(index)) continue;
3111
3319
  const declared = route.flags.find((candidate)=>key(candidate.flag) === key(flag.name));
3112
3320
  if (declared) given.set(declared.flag, flag);
3113
3321
  else if (!unknown.has(flag.name)) {
@@ -3133,7 +3341,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
3133
3341
  value: controller_decorator.userWords(value)
3134
3342
  }
3135
3343
  }, flag.flag));
3136
- else params[flag.flag] = on;
3344
+ else controller_decorator.setOwn(params, flag.flag, on);
3137
3345
  } else if (!given.has(flag.flag)) {
3138
3346
  if (!flag.optional) issues.push(controller_decorator.usageIssue({
3139
3347
  key: 'meocord.usage.missing',
@@ -3161,29 +3369,137 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
3161
3369
  return issues;
3162
3370
  }
3163
3371
  /**
3164
- * What is wrong with a command whose words a message named but whose pattern it does not fit, given how many
3165
- * words followed the command words: each required param it left out, or words beyond what the pattern takes.
3166
- */ function missingParams(route, given) {
3167
- const firstParam = route.tokens.findIndex((token)=>!('literal' in token));
3168
- const after = firstParam === -1 ? [] : route.tokens.slice(firstParam);
3169
- const missing = after.slice(given).filter((token)=>'param' in token && !token.optional).map((token)=>controller_decorator.usageIssue({
3170
- key: 'meocord.usage.missing',
3171
- params: {
3172
- param: token.param
3372
+ * What is wrong with a command whose words a message named but whose pattern it does not fit, read word by word
3373
+ * against the pattern: each word or required param it left out, a word given another in its place, else words
3374
+ * beyond what the pattern takes, else the first word its param's built-in type does not take; and then each required
3375
+ * flag it left out, as a message whose words fit is told. With none of those true, there are none, and the usage stands
3376
+ * alone.
3377
+ */ function missingParams(route, given, types, guild) {
3378
+ const { words, flags } = given;
3379
+ const missing = [];
3380
+ // The flags given bare in place of the pattern's own `--` words, which are those words and set no flag
3381
+ const literal = new Set();
3382
+ let unfit;
3383
+ let w = 0;
3384
+ for (const token of route.tokens){
3385
+ if ('literal' in token) {
3386
+ if (w < words.length && caseKey(words[w].value, route.caseSensitive) === caseKey(token.literal, route.caseSensitive)) {
3387
+ w++;
3388
+ continue;
3173
3389
  }
3174
- }, token.param));
3175
- if (missing.length > 0) return missing;
3176
- return [
3390
+ // A pattern's own `--` word given bare in its place takes no word; another word in its place is not taken
3391
+ const word = flags.find((flag)=>!literal.has(flag) && isLiteralFlag(flag, token.literal, w, route.caseSensitive));
3392
+ if (word) literal.add(word);
3393
+ else missing.push(controller_decorator.usageIssue({
3394
+ key: 'meocord.usage.missing',
3395
+ params: {
3396
+ param: token.literal
3397
+ }
3398
+ }));
3399
+ } else if (w === words.length) {
3400
+ if (!token.optional) missing.push(controller_decorator.usageIssue({
3401
+ key: 'meocord.usage.missing',
3402
+ params: {
3403
+ param: token.param
3404
+ }
3405
+ }, token.param));
3406
+ } else if (token.rest) {
3407
+ w = words.length;
3408
+ } else {
3409
+ // Words fill the params in order, as the usage shows them
3410
+ const word = words[w++].value;
3411
+ const type = token.type;
3412
+ if (!unfit && type !== undefined && isUnfit(type, word, route.caseSensitive, guild)) {
3413
+ unfit = {
3414
+ key: token.param,
3415
+ label: token.param,
3416
+ type,
3417
+ word
3418
+ };
3419
+ }
3420
+ }
3421
+ }
3422
+ // With none of these, the reason may be one MeoCord has no text for, as a role's name the matcher takes only as a
3423
+ // mention or an ID
3424
+ const reasons = missing.length > 0 ? missing : w < words.length ? [
3177
3425
  controller_decorator.usageIssue({
3178
3426
  key: 'meocord.usage.tooManyWords'
3179
3427
  })
3428
+ ] : unfit ? [
3429
+ wrongType(unfit, types)
3430
+ ] : [];
3431
+ // So one reply names all the message lacks, rather than the flags only once its words fit
3432
+ const missingFlags = route.flags.filter((flag)=>flag.type !== undefined && !flag.optional && !flags.some((sent)=>!literal.has(sent) && caseKey(sent.name, route.caseSensitive) === caseKey(flag.flag, route.caseSensitive))).map((flag)=>controller_decorator.usageIssue({
3433
+ key: 'meocord.usage.missing',
3434
+ params: {
3435
+ param: `--${flag.flag}`
3436
+ }
3437
+ }, flag.flag));
3438
+ return [
3439
+ ...reasons,
3440
+ ...missingFlags
3180
3441
  ];
3181
3442
  }
3443
+ /**
3444
+ * Whether a param of a type is known not to take a word: a built-in type or words to choose from that the word doesn't
3445
+ * fit. A role is also found by its name, so only a word that names none of the server's roles is unfit.
3446
+ */ function isUnfit(type, word, caseSensitive, guild) {
3447
+ if (type === 'role') return guild !== null && roleNamed(guild, word) === undefined;
3448
+ return (type in BUILT_IN_TYPES || Boolean(controller_decorator.choicesOf(type))) && !fitsParamType(type, word, caseSensitive);
3449
+ }
3182
3450
  /** Whether a route's params need resolving: it declares a type for one, or has flags. */ const hasTypedParams = (route)=>route.flags.length > 0 || route.tokens.some((token)=>'param' in token && token.type !== undefined);
3183
3451
 
3452
+ const logger = new controller_decorator.Logger('MeoCord');
3184
3453
  /** Whether a route's scope lets it run where the message was sent. */ const fitsScope = (scope, inGuild)=>inGuild === undefined || scope === 'any' || scope === 'guild' === inGuild;
3185
3454
  const PARAM = /^\{(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\.\.\.)?(\?)?\}$/;
3186
3455
  const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3456
+ /**
3457
+ * A whole brace pair that PARAM or FLAG refused, read loosely to tell its name from its type: anything up to `:` or `}`,
3458
+ * then the marks a param ends with, `...` and `?`, wherever they were written.
3459
+ */ const LOOSE_FLAG = /^\{--([^:}]*?)(?::([^}]*?))?(\.\.\.)?(\?)?\}$/;
3460
+ const LOOSE_PARAM = /^\{([^:}]*?)(?::([^}]*?))?(\.\.\.)?(\?)?\}$/;
3461
+ /** A name or type with marks after it, as one written out of order carries them. */ const MARKED = /^(.*?)((?:\.\.\.|\?)+)$/;
3462
+ /** A param's name, and a flag's, which also starts with a letter, as a message could not give it otherwise. */ const PARAM_NAME = /^\w+$/;
3463
+ const FLAG_NAME = /^[A-Za-z]\w*$/;
3464
+ /** A param as its pattern writes it, which the grammar spells only one way. */ const writtenParam = (token)=>`{${token.param}${token.type ? `:${token.type}` : ''}${token.rest ? '...' : ''}${token.optional ? '?' : ''}}`;
3465
+ /** A flag as its pattern writes it. */ const writtenFlag = (flag)=>`{--${flag.flag}${flag.type ? `:${flag.type}` : ''}${flag.optional ? '?' : ''}}`;
3466
+ /**
3467
+ * Why a word with a brace in it is no param or flag: text around its braces, or a whole pair whose name, type or marks
3468
+ * the syntax doesn't take. Each names the rule broken, with the word mended as an example.
3469
+ */ function notAParam(word) {
3470
+ const flag = LOOSE_FLAG.exec(word);
3471
+ if (flag) {
3472
+ const [, name, type, rest] = flag;
3473
+ if (rest || word.includes('...')) {
3474
+ return `"${word}" is not a flag: a flag doesn't take the rest of the message; a param does, such as {${mended(unmarked(name), 'param')}...}.`;
3475
+ }
3476
+ if (unmarked(name) !== name || type !== undefined && unmarked(type) !== type) {
3477
+ const typed = type === undefined ? '' : `:${unmarked(type)}`;
3478
+ return `"${word}" is not a flag: its ? goes last, as in {--${unmarked(name)}${typed}?}.`;
3479
+ }
3480
+ if (!FLAG_NAME.test(name)) {
3481
+ return `"${word}" is not a flag: a flag's name starts with a letter, then letters, digits or _, such as {--${mended(name, 'flag')}}.`;
3482
+ }
3483
+ return `"${word}" is not a flag: a type is a name, or words to choose from joined by |, such as {--${name}:on|off}.`;
3484
+ }
3485
+ const param = LOOSE_PARAM.exec(word);
3486
+ if (param) {
3487
+ const [, name, type] = param;
3488
+ if (unmarked(name) !== name || type !== undefined && unmarked(type) !== type) {
3489
+ const typed = type === undefined ? '' : `:${unmarked(type)}`;
3490
+ const ordered = `{${unmarked(name)}${typed}${word.includes('...') ? '...' : ''}${word.includes('?') ? '?' : ''}}`;
3491
+ return `"${word}" is not a param: its marks go last, ... then ?, as in ${ordered}.`;
3492
+ }
3493
+ if (!PARAM_NAME.test(name)) return `"${word}" is not a param: a param's name is ASCII letters, digits and _, such as {${mended(name, 'param')}}.`;
3494
+ return `"${word}" is not a param: a type is a name, or words to choose from joined by |, such as {${name}:on|off}.`;
3495
+ }
3496
+ return `"${word}" is not a param: a param is a whole word, such as {name}, {name:type}, {name...}, {name?} or a flag, {--name}.`;
3497
+ }
3498
+ /** A name or type without the marks written after it, `...` and `?`. */ const unmarked = (text)=>MARKED.exec(text)?.[1] ?? text;
3499
+ /** A name with each character the syntax doesn't take as `_`, and a flag's led by a letter, for an example. */ function mended(name, kind) {
3500
+ const word = name.replace(/\W/g, '_') || kind;
3501
+ return kind === 'flag' && !/^[A-Za-z]/.test(word) ? `flag${word}` : word;
3502
+ }
3187
3503
  /**
3188
3504
  * Reads a pattern into its words and flags. Throws for a pattern of flags alone, a param that is not a whole word, a
3189
3505
  * name given twice, a rest param that is not last, a word after an optional param that is not optional too, an untyped
@@ -3219,9 +3535,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3219
3535
  words.forEach((word, index)=>{
3220
3536
  const match = PARAM.exec(word);
3221
3537
  if (!match) {
3222
- if (/[{}]/.test(word)) {
3223
- throw new Error(`"${word}" is not a param: a param is a whole word, such as {name}, {name:type}, {name...}, {name?} or a flag, {--name}.`);
3224
- }
3538
+ if (/[{}]/.test(word)) throw new Error(notAParam(word));
3225
3539
  tokens.push({
3226
3540
  literal: word
3227
3541
  });
@@ -3244,7 +3558,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3244
3558
  const next = tokens[index + 1];
3245
3559
  if (!('param' in token) || !token.optional || !next) return;
3246
3560
  if (!('param' in next) || !next.optional) {
3247
- throw new Error(`{${token.param}?} is optional, so only optional params may follow it; "${'param' in next ? `{${next.param}}` : next.literal}" is not.`);
3561
+ throw new Error(`${writtenParam(token)} is optional, so only optional params may follow it; "${'param' in next ? writtenParam(next) : next.literal}" is not.`);
3248
3562
  }
3249
3563
  if (token.type === undefined || token.type === 'string') {
3250
3564
  throw new Error(`{${token.param}${token.type ? ':string' : ''}?} comes before another optional param, so it needs a type that tells its words ` + `from the next param's, such as {${token.param}:int?}: as text, it would take every word.`);
@@ -3257,10 +3571,15 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3257
3571
  rest: params.some((token)=>token.rest),
3258
3572
  optional: params.some((token)=>token.optional)
3259
3573
  });
3574
+ const inheritedNames = [
3575
+ ...params.map((token)=>token.param),
3576
+ ...flags.map((flag)=>flag.flag)
3577
+ ].filter((name)=>name in Object.prototype);
3260
3578
  return {
3261
3579
  tokens,
3262
3580
  flags,
3263
- specificity
3581
+ specificity,
3582
+ inheritedNames
3264
3583
  };
3265
3584
  }
3266
3585
  /** A prefix setting as a list, where `''` stands for none. */ function prefixList(prefix) {
@@ -3311,7 +3630,20 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3311
3630
  ...mention
3312
3631
  ]);
3313
3632
  }
3314
- /** Whether two routes accept some of the same messages: a start they share, and words that match alike. */ function sameMessages(a, b, options) {
3633
+ /** Where two routes of equal rank and shape part: the one with a required param where the other has an optional one comes first. */ function compareOptional(a, b) {
3634
+ for(let i = 0; i < Math.min(a.length, b.length); i++){
3635
+ const [aToken, bToken] = [
3636
+ a[i],
3637
+ b[i]
3638
+ ];
3639
+ if ('param' in aToken && 'param' in bToken && aToken.optional !== bToken.optional) return aToken.optional ? 1 : -1;
3640
+ }
3641
+ return 0;
3642
+ }
3643
+ /**
3644
+ * Whether two routes accept some of the same messages: a start they share, and words that match alike. `'optional'`
3645
+ * when they do only because a required param and an optional one, in one position, both take a word there.
3646
+ */ function sameMessages(a, b, options) {
3315
3647
  // A case-insensitive word matches every message the case-sensitive one does
3316
3648
  const exact = a.caseSensitive && b.caseSensitive;
3317
3649
  const key = (start)=>typeof start === 'string' && !exact ? start.toLowerCase() : start;
@@ -3329,13 +3661,55 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3329
3661
  ].some((start)=>theirs.has(key(start)));
3330
3662
  });
3331
3663
  if (!startAlike || a.tokens.length !== b.tokens.length) return false;
3332
- return a.tokens.every((token, i)=>{
3664
+ let byOptional = false;
3665
+ const alike = a.tokens.every((token, i)=>{
3333
3666
  const other = b.tokens[i];
3334
3667
  if ('literal' in token) {
3335
3668
  return 'literal' in other && (exact ? token.literal === other.literal : token.literal.toLowerCase() === other.literal.toLowerCase());
3336
3669
  }
3337
- return 'param' in other && token.rest === other.rest && token.optional === other.optional;
3670
+ if (!('param' in other) || token.rest !== other.rest) return false;
3671
+ if (token.optional !== other.optional) byOptional = true;
3672
+ return true;
3338
3673
  });
3674
+ return alike && (byOptional ? 'optional' : true);
3675
+ }
3676
+ /** What a static empty prefix list does now, which a prefix function's `[]` doesn't: take messages as they are. */ const EMPTY_PREFIX_LIST = "`[]` takes messages as they are, with no prefix; write `''` for that";
3677
+ /**
3678
+ * The warnings for message handlers a bot starts with today though a message can't reach them as written, each once,
3679
+ * for the app or the handler: a static empty prefix list, which takes messages as they are, where a prefix function's
3680
+ * `[]` starts no command; and two flags of a command that reads flags in any case whose names differ only in case, the
3681
+ * second of which is never set. The next major version (5.0) changes the first and refuses the second.
3682
+ */ function messageRouteWarnings(routes, options) {
3683
+ const warnings = [];
3684
+ if (Array.isArray(options.prefix) && options.prefix.length === 0) {
3685
+ warnings.push(`@MeoCord({ messages: { prefix: [] } }): ${EMPTY_PREFIX_LIST}. In the next major version (5.0), \`[]\` starts no command, ` + "as a prefix function's `[]` does.");
3686
+ }
3687
+ const seen = new Map();
3688
+ for (const route of routes){
3689
+ // A handler's aliases share its options and flags, so each is read once; one method can carry several handlers
3690
+ const pattern = route.aliasOf ?? route.pattern;
3691
+ const methods = seen.get(route.controllerClass) ?? seen.set(route.controllerClass, new Map()).get(route.controllerClass);
3692
+ const patterns = methods.get(route.method) ?? methods.set(route.method, new Set()).get(route.method);
3693
+ if (patterns.has(pattern)) continue;
3694
+ patterns.add(pattern);
3695
+ const handler = `${route.controllerClass.name}.${route.method}: @MessageHandler('${pattern}'`;
3696
+ const own = controller_decorator.getMessageHandlers(route.controllerClass.prototype).find((candidate)=>candidate.method === route.method && candidate.pattern === pattern)?.options;
3697
+ if (Array.isArray(own?.prefix) && own.prefix.length === 0) {
3698
+ warnings.push(`${handler}, { prefix: [] }): ${EMPTY_PREFIX_LIST}, or \`false\` for no prefix and no mention. In the next major ` + 'version (5.0), `[]` starts no command.');
3699
+ }
3700
+ if (route.caseSensitive) continue;
3701
+ const first = new Map();
3702
+ for (const flag of route.flags){
3703
+ const same = first.get(flag.flag.toLowerCase());
3704
+ if (!same) {
3705
+ first.set(flag.flag.toLowerCase(), flag);
3706
+ continue;
3707
+ }
3708
+ warnings.push(`${handler}): ${writtenFlag(same)} and ${writtenFlag(flag)} differ only in case, and this command reads flags in any ` + `case, so --${flag.flag} always sets ${writtenFlag(same)} and ${writtenFlag(flag)} is never set. Rename one, or set ` + 'caseSensitive: true. The next major version (5.0) refuses this.');
3709
+ break;
3710
+ }
3711
+ }
3712
+ return warnings;
3339
3713
  }
3340
3714
  /**
3341
3715
  * Every patterned message handler of the given controllers, most specific first, read from metadata
@@ -3404,14 +3778,21 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3404
3778
  });
3405
3779
  }
3406
3780
  }
3407
- routes.sort((a, b)=>b.specificity - a.specificity || compareShapes(a.tokens, b.tokens) || Number(takesAppStart(a)) - Number(takesAppStart(b)));
3781
+ routes.sort((a, b)=>b.specificity - a.specificity || compareShapes(a.tokens, b.tokens) || compareOptional(a.tokens, b.tokens) || Number(takesAppStart(a)) - Number(takesAppStart(b)));
3408
3782
  for(let i = 0; i < routes.length; i++){
3409
3783
  for(let j = i + 1; j < routes.length; j++){
3410
3784
  const [a, b] = [
3411
3785
  routes[i],
3412
3786
  routes[j]
3413
3787
  ];
3414
- if (!sameMessages(a, b, options)) continue;
3788
+ const same = sameMessages(a, b, options);
3789
+ if (!same) continue;
3790
+ // Ranked by their params, so the same one answers whatever the order; 5.0 refuses these as it does the rest
3791
+ if (same === 'optional') {
3792
+ if (a.controllerClass === b.controllerClass && a.method === b.method) continue;
3793
+ controller_decorator.warnOnce(logger, `${a.controllerClass.name}.${a.method}: ${describeRoute(a)} and ${describeRoute(b)} in ${b.controllerClass.name}.${b.method} can ` + `take the same messages, as one takes a word where the other has an optional param; ${a.controllerClass.name}.${a.method} ` + 'answers those. Change one pattern, or give one its own prefix. The next major version (5.0) stops the bot at startup for these.');
3794
+ continue;
3795
+ }
3415
3796
  // One handler under two spellings, such as 'hello' and 'Hello', is one route, its pattern's over an alias's
3416
3797
  if (a.controllerClass === b.controllerClass && a.method === b.method) {
3417
3798
  if (a.aliasOf && !b.aliasOf) routes[i] = b;
@@ -3456,7 +3837,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
3456
3837
  if (scope !== 'dm') return;
3457
3838
  const param = tokens.find((token)=>'param' in token && token.type !== undefined && isGuildType(token.type));
3458
3839
  const flag = flags.find((candidate)=>candidate.type !== undefined && isGuildType(candidate.type));
3459
- const shown = param ? `{${param.param}:${param.type}}` : flag && `{--${flag.flag}:${flag.type}}`;
3840
+ const shown = param ? writtenParam(param) : flag && writtenFlag(flag);
3460
3841
  if (shown) throw new Error(`scope is 'dm', but ${shown} is found only in a server.`);
3461
3842
  }
3462
3843
  /** Refuses a handler's `mention` other than `'only'`. */ function assertMention({ mention }) {
@@ -3568,11 +3949,11 @@ const trieNode = ()=>({
3568
3949
  for(let t = 0; t < tail.length && w < words.length; t++){
3569
3950
  const token = tail[t];
3570
3951
  if (token.rest) {
3571
- params[token.param] = restFrom(text, words[w].start, cuts);
3952
+ controller_decorator.setOwn(params, token.param, restFrom(text, words[w].start, cuts));
3572
3953
  return params;
3573
3954
  }
3574
3955
  if (t < tail.length - 1 && !fitsParamType(token.type, words[w].value, caseSensitive)) continue;
3575
- params[token.param] = words[w++].value;
3956
+ controller_decorator.setOwn(params, token.param, words[w++].value);
3576
3957
  }
3577
3958
  return w === words.length ? params : undefined;
3578
3959
  }
@@ -3617,9 +3998,12 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
3617
3998
  return;
3618
3999
  }
3619
4000
  if ('literal' in token) {
4001
+ var _node1;
3620
4002
  const key = wordKey(token.literal, route.caseSensitive);
3621
4003
  let next = node.words.get(key);
3622
4004
  if (!next) node.words.set(key, next = trieNode());
4005
+ // Such a word typed bare is taken out as a flag before the words are walked, so it is matched as one too
4006
+ if (route.flags.length > 0 && token.literal.startsWith('--')) ((_node1 = node).flagWords ?? (_node1.flagWords = new Map())).set(key, next);
3623
4007
  node = next;
3624
4008
  continue;
3625
4009
  }
@@ -3638,6 +4022,8 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
3638
4022
  ];
3639
4023
  for (const group of all){
3640
4024
  const flagged = group.flagged;
4025
+ // A pattern's own leading `--` word stays reachable there: given bare, it is taken out as a flag, so the message
4026
+ // seems to start with one
3641
4027
  if (flagged) group.flaggedAfterFlag = {
3642
4028
  ...flagged,
3643
4029
  words: new Map()
@@ -3678,7 +4064,16 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
3678
4064
  if (first === undefined || node.param || node.rests.length > 0 || node.tails.length > 0 || node.ends.length > 0) return true;
3679
4065
  return node.words.has(first);
3680
4066
  }
3681
- /** The ranks of every route the words reach from `node`. Each node sits at one word depth, so each is visited once. */ function reach(node, words, i, text, cuts, caseSensitive, found) {
4067
+ /**
4068
+ * The ranks of every route the words reach from `node`. A pattern's own `--` word is reached by that word, or by a flag
4069
+ * of its name given bare in its place, which takes no word.
4070
+ */ function reach(node, words, i, text, cuts, caseSensitive, flags, found) {
4071
+ if (node.flagWords) {
4072
+ for (const flag of flags){
4073
+ const next = flag.at === i && flag.value === undefined ? node.flagWords.get(wordKey(`--${flag.name}`, caseSensitive)) : undefined;
4074
+ if (next) reach(next, words, i, text, cuts, caseSensitive, flags, found);
4075
+ }
4076
+ }
3682
4077
  for (const tail of node.tails)if (assignTail(tail.tokens, words, i, text, cuts, caseSensitive)) found.push(tail.rank);
3683
4078
  if (i === words.length) {
3684
4079
  for (const rank of node.ends)found.push(rank);
@@ -3686,22 +4081,34 @@ const wordKey = (word, caseSensitive)=>caseSensitive ? word : word.toLowerCase()
3686
4081
  }
3687
4082
  for (const rank of node.rests)found.push(rank);
3688
4083
  const next = node.words.get(wordKey(words[i].value, caseSensitive));
3689
- if (next) reach(next, words, i + 1, text, cuts, caseSensitive, found);
3690
- if (node.param) reach(node.param, words, i + 1, text, cuts, caseSensitive, found);
4084
+ if (next) reach(next, words, i + 1, text, cuts, caseSensitive, flags, found);
4085
+ if (node.param) reach(node.param, words, i + 1, text, cuts, caseSensitive, flags, found);
3691
4086
  }
3692
- /** The params a route's words capture, given words its pattern is known to match. */ function paramsOf$1(route, words, text, cuts) {
4087
+ /**
4088
+ * The params a route's words capture, given words its pattern is known to match. A pattern's own `--` word that the
4089
+ * message gave as a flag, one of `taken`, takes no word.
4090
+ */ function paramsOf$1(route, words, text, cuts, taken) {
3693
4091
  const tail = tailStart(route.tokens);
3694
- const params = tail === -1 ? {} : assignTail(route.tokens.slice(tail), words, tail, text, cuts, route.caseSensitive);
3695
- for(let i = 0; i < (tail === -1 ? route.tokens.length : tail) && i < words.length; i++){
4092
+ const params = {};
4093
+ let left = taken;
4094
+ let w = 0;
4095
+ for(let i = 0; i < (tail === -1 ? route.tokens.length : tail) && w < words.length; i++){
3696
4096
  const token = route.tokens[i];
3697
- if ('literal' in token) continue;
4097
+ if ('literal' in token) {
4098
+ if (left > 0 && token.literal.startsWith('--') && wordKey(words[w].value, route.caseSensitive) !== wordKey(token.literal, route.caseSensitive)) left--;
4099
+ else w++;
4100
+ continue;
4101
+ }
3698
4102
  if (token.rest) {
3699
- params[token.param] = restFrom(text, words[i].start, cuts);
3700
- break;
4103
+ controller_decorator.setOwn(params, token.param, restFrom(text, words[w].start, cuts));
4104
+ return params;
3701
4105
  }
3702
- params[token.param] = words[i].value;
4106
+ controller_decorator.setOwn(params, token.param, words[w++].value);
3703
4107
  }
3704
- return params;
4108
+ return tail === -1 ? params : {
4109
+ ...params,
4110
+ ...assignTail(route.tokens.slice(tail), words, w, text, cuts, route.caseSensitive)
4111
+ };
3705
4112
  }
3706
4113
  /**
3707
4114
  * The text after the start a message used for the group's routes, or `undefined` when it used none: a mention
@@ -3754,7 +4161,7 @@ const indexes = new WeakMap();
3754
4161
  const found = [];
3755
4162
  if (mayEnter(group.root, first)) {
3756
4163
  const words = wordsOf(rest);
3757
- reach(group.root, words, 0, rest, NONE, group.caseSensitive, found);
4164
+ reach(group.root, words, 0, rest, NONE, group.caseSensitive, NONE, found);
3758
4165
  for (const rank of found){
3759
4166
  if (fitsScope(routes[rank].scope, starts.inGuild)) {
3760
4167
  if (!best || rank < best.rank) best = {
@@ -3783,7 +4190,7 @@ const indexes = new WeakMap();
3783
4190
  };
3784
4191
  found.length = 0;
3785
4192
  const from = scanned.words[0]?.start === 0 ? flagged : group.flaggedAfterFlag;
3786
- reach(from, scanned.words, 0, rest, scanned.cuts, group.caseSensitive, found);
4193
+ reach(from, scanned.words, 0, rest, scanned.cuts, group.caseSensitive, scanned.flags, found);
3787
4194
  for (const rank of found){
3788
4195
  const candidate = {
3789
4196
  rank,
@@ -3800,11 +4207,16 @@ const indexes = new WeakMap();
3800
4207
  const chosen = best ?? outside;
3801
4208
  if (!chosen) return undefined;
3802
4209
  const route = routes[chosen.rank];
3803
- const params = paramsOf$1(route, chosen.words, chosen.rest, chosen.cuts);
3804
- for (const { name, value } of chosen.flags){
4210
+ // The flags the pattern's own `--` words took are those words, and set none of its flags
4211
+ const taken = literalFlags(route.tokens, route.caseSensitive, chosen.words, chosen.flags);
4212
+ const params = paramsOf$1(route, chosen.words, chosen.rest, chosen.cuts, taken.size);
4213
+ chosen.flags.forEach(({ name, value }, index)=>{
4214
+ if (taken.has(index)) return;
3805
4215
  const declared = route.flags.find((flag)=>wordKey(flag.flag, route.caseSensitive) === wordKey(name, route.caseSensitive));
3806
- if (declared) params[declared.flag] = value ?? '';
3807
- }
4216
+ if (declared) controller_decorator.setOwn(params, declared.flag, value ?? '');
4217
+ });
4218
+ // A param or flag left out reads as left out, not as what Object.prototype has under its name
4219
+ if (route.inheritedNames.length > 0) controller_decorator.shadowInherited(params, route.inheritedNames);
3808
4220
  return {
3809
4221
  route,
3810
4222
  params,
@@ -3834,19 +4246,28 @@ const indexes = new WeakMap();
3834
4246
  if (first !== undefined && !group.commands.has(first)) continue;
3835
4247
  const plain = splitWords(rest);
3836
4248
  // A route with flags counts the words left once the message's flags are taken out
3837
- const positional = mayEnter(group.flagged, first) && rest.includes('--') ? splitFlagWords(rest).words : plain;
4249
+ const scanned = mayEnter(group.flagged, first) && rest.includes('--') ? splitFlagWords(rest) : {
4250
+ words: plain,
4251
+ flags: NONE
4252
+ };
3838
4253
  const candidates = group.commands.get(wordKey(plain[0]?.value ?? '', group.caseSensitive)) ?? [];
3839
4254
  for (const rank of candidates){
3840
4255
  if (best && rank >= best.rank) break;
3841
- const words = routes[rank].flags.length > 0 ? positional : plain;
3842
- const command = commandWordsOf(routes[rank].tokens);
3843
- if (command.every((word, i)=>words[i] && wordKey(words[i].value, group.caseSensitive) === wordKey(word, group.caseSensitive))) {
4256
+ const flagged = routes[rank].flags.length > 0;
4257
+ const words = flagged ? scanned.words : plain;
4258
+ const flags = flagged ? scanned.flags : NONE;
4259
+ const taken = commandWordsTaken(commandWordsOf(routes[rank].tokens), words, flags, group.caseSensitive);
4260
+ if (taken !== -1) {
3844
4261
  const named = {
3845
4262
  rank,
3846
4263
  start: text.slice(0, text.length - rest.length),
3847
- given: words.length - command.length
4264
+ given: {
4265
+ words,
4266
+ flags
4267
+ }
3848
4268
  };
3849
- if (fitsScope(routes[rank].scope, starts.inGuild)) {
4269
+ // Where it works as dispatch answers it, so a DM's usage comes from a route that works there
4270
+ if (fitsScope(effectiveScope(routes[rank]), starts.inGuild)) {
3850
4271
  best = named;
3851
4272
  break;
3852
4273
  }
@@ -3861,6 +4282,17 @@ const indexes = new WeakMap();
3861
4282
  given: chosen.given
3862
4283
  };
3863
4284
  }
4285
+ /**
4286
+ * How many of a message's words a route's command words take, or -1 when the message names another command. A
4287
+ * command's own `--` word may be given bare as a flag in its place, and takes no word.
4288
+ */ function commandWordsTaken(command, words, flags, caseSensitive) {
4289
+ let w = 0;
4290
+ for (const word of command){
4291
+ if (words[w] && wordKey(words[w].value, caseSensitive) === wordKey(word, caseSensitive)) w++;
4292
+ else if (!flags.some((flag)=>isLiteralFlag(flag, word, w, caseSensitive))) return -1;
4293
+ }
4294
+ return w;
4295
+ }
3864
4296
  /**
3865
4297
  * The subcommands a message names the parent of, when no pattern matches it and it names no command: the routes whose
3866
4298
  * command words begin with the most of the message's words, short of all of them, as `!config` does for `config set …`
@@ -3903,11 +4335,18 @@ const indexes = new WeakMap();
3903
4335
  }
3904
4336
  return found.length > 0 ? found : undefined;
3905
4337
  }
4338
+ /**
4339
+ * Whether a message has text a handler could take; dispatch runs no handler for one without. Content that isn't a
4340
+ * string, as a mock's placeholder without strict mocks, is no text.
4341
+ */ function hasMessageText(content) {
4342
+ return typeof content === 'string' && content.trim() !== '';
4343
+ }
3906
4344
  /**
3907
4345
  * What a test calling a handler with a message alone passes as its params, as dispatch builds them: `undefined` for a
3908
- * listener, `{}` for a message without content, else what the pattern captures with the route and start; for one naming
3909
- * the command without fitting it, the route, start and `given`, the words after the command words. Either only when
3910
- * dispatch would give the message to this handler among `controllers`; otherwise why it does not reach the handler.
4346
+ * listener, `{}` for a message whose content isn't a string, as a mock's without strict mocks, else what the pattern
4347
+ * captures with the route and start; for one naming the command without fitting it, the route, start and `given`, the
4348
+ * message's words and flags. Either only when dispatch would give the message to this handler among
4349
+ * `controllers`; otherwise why it does not reach the handler, a message with no text included.
3911
4350
  */ async function messageParamsFor(controllerClass, methodName, message, options, controllers = [
3912
4351
  controllerClass
3913
4352
  ]) {
@@ -3919,6 +4358,13 @@ const indexes = new WeakMap();
3919
4358
  if (typeof message.content !== 'string') return {
3920
4359
  params: {}
3921
4360
  };
4361
+ const handler = `${controllerClass.name}.${methodName}`;
4362
+ const pattern = routes.find((route)=>!route.aliasOf)?.pattern ?? routes[0].pattern;
4363
+ if (!hasMessageText(message.content)) {
4364
+ return {
4365
+ mismatch: `message has no text, so dispatch runs no handler for it: give the mock the content ${handler}'s pattern '${pattern}' takes.`
4366
+ };
4367
+ }
3922
4368
  const botId = message.client?.user?.id;
3923
4369
  const all = buildMessageRoutes(controllers, options);
3924
4370
  const starts = await messageStartsFor(all, options, message, typeof botId === 'string' ? botId : undefined);
@@ -3927,7 +4373,7 @@ const indexes = new WeakMap();
3927
4373
  if (target && (target.route.controllerClass !== controllerClass || target.route.method !== methodName)) {
3928
4374
  const other = `${target.route.controllerClass.name}.${target.route.method}`;
3929
4375
  return {
3930
- mismatch: `message '${message.content}' does not reach ${controllerClass.name}.${methodName}: dispatch runs ${other}.`
4376
+ mismatch: `message '${message.content}' does not reach ${handler}: dispatch runs ${other}.`
3931
4377
  };
3932
4378
  }
3933
4379
  if (target && 'params' in target) return {
@@ -3941,9 +4387,8 @@ const indexes = new WeakMap();
3941
4387
  start: target.start,
3942
4388
  given: target.given
3943
4389
  };
3944
- const pattern = routes.find((route)=>!route.aliasOf)?.pattern ?? routes[0].pattern;
3945
4390
  return {
3946
- mismatch: `message '${message.content}' does not match ${controllerClass.name}.${methodName}'s pattern '${pattern}'.`
4391
+ mismatch: `message '${message.content}' does not match ${handler}'s pattern '${pattern}'.`
3947
4392
  };
3948
4393
  }
3949
4394
 
@@ -3972,12 +4417,13 @@ const indexes = new WeakMap();
3972
4417
  const text = content.trim();
3973
4418
  const rest = afterStart(text, starts.prefixes.filter((prefix)=>prefix !== ''), starts.mention, caseSensitive);
3974
4419
  if (!rest) return undefined;
3975
- const [first] = splitWords(rest);
4420
+ const [first, second] = splitWords(rest);
3976
4421
  const key = (word)=>caseSensitive ? word : word.toLowerCase();
3977
4422
  if (!first || !words.some((word)=>key(word) === key(first.value))) return undefined;
4423
+ // From where the next word starts, as the help word may have been written in quotes
3978
4424
  return {
3979
4425
  start: text.slice(0, text.length - rest.length),
3980
- query: rest.slice(first.value.length).trim()
4426
+ query: second ? rest.slice(second.start).trim() : ''
3981
4427
  };
3982
4428
  }
3983
4429
  /** The start a caller would type a route with here: its own prefix, a mention, or the start the help message used. */ function displayStart(route, start, starts) {
@@ -3986,11 +4432,6 @@ const indexes = new WeakMap();
3986
4432
  if (route.prefix !== undefined) return route.prefix[0] ?? '';
3987
4433
  return start;
3988
4434
  }
3989
- /** Where a route works: its scope, narrowed to servers by a `member`, `role` or `channel` param or flag. */ function effectiveScope(route) {
3990
- if (route.scope !== 'any') return route.scope;
3991
- const guildOnly = route.tokens.some((token)=>'param' in token && token.type !== undefined && isGuildType(token.type)) || route.flags.some((flag)=>flag.type !== undefined && isGuildType(flag.type));
3992
- return guildOnly ? 'guild' : 'any';
3993
- }
3994
4435
  /** A route's params and flags, each with what it takes in words. */ function paramsOf(route, types, render) {
3995
4436
  const params = route.tokens.flatMap((token)=>{
3996
4437
  if (!('param' in token)) return [];
@@ -4190,22 +4631,48 @@ const sortedEntries = (entries)=>entries.sort((a, b)=>a.usage < b.usage ? -1 : a
4190
4631
  scope
4191
4632
  ].filter((line)=>line !== undefined).join('\n');
4192
4633
  }
4193
- /** Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. */ function splitReply(text) {
4634
+ // Built on first use, for a line longer than a message
4635
+ let graphemes;
4636
+ /**
4637
+ * Where a line longer than one message is cut: after the last character a reader sees as one, such as an emoji, a flag
4638
+ * or a letter with its accent, that still fits. A single such character longer than a message is cut between code points.
4639
+ */ function fittingCut(piece) {
4640
+ let cut = 0;
4641
+ graphemes ?? (graphemes = new Intl.Segmenter(undefined, {
4642
+ granularity: 'grapheme'
4643
+ }));
4644
+ for (const { index, segment } of graphemes.segment(piece)){
4645
+ if (index + segment.length > MAX_REPLY_LENGTH) break;
4646
+ cut = index + segment.length;
4647
+ }
4648
+ if (cut > 0) return cut;
4649
+ const high = piece.charCodeAt(MAX_REPLY_LENGTH - 1);
4650
+ return high >= 0xd800 && high <= 0xdbff ? MAX_REPLY_LENGTH - 1 : MAX_REPLY_LENGTH;
4651
+ }
4652
+ /**
4653
+ * Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. A
4654
+ * message of only whitespace, which Discord refuses as empty, is left out.
4655
+ */ function splitReply(text) {
4194
4656
  const chunks = [];
4657
+ const push = (chunk)=>{
4658
+ if (chunk.trim() !== '') chunks.push(chunk);
4659
+ };
4195
4660
  let current = '';
4196
4661
  for (const line of text.split('\n')){
4197
- for(let piece = line;; piece = piece.slice(MAX_REPLY_LENGTH)){
4198
- const part = piece.slice(0, MAX_REPLY_LENGTH);
4662
+ for(let piece = line;;){
4663
+ const cut = piece.length <= MAX_REPLY_LENGTH ? piece.length : fittingCut(piece);
4664
+ const part = piece.slice(0, cut);
4199
4665
  const joined = current ? `${current}\n${part}` : part;
4200
4666
  if (joined.length <= MAX_REPLY_LENGTH) current = joined;
4201
4667
  else {
4202
- chunks.push(current);
4668
+ push(current);
4203
4669
  current = part;
4204
4670
  }
4205
- if (piece.length <= MAX_REPLY_LENGTH) break;
4671
+ if (cut === piece.length) break;
4672
+ piece = piece.slice(cut);
4206
4673
  }
4207
4674
  }
4208
- if (current) chunks.push(current);
4675
+ push(current);
4209
4676
  return chunks;
4210
4677
  }
4211
4678
 
@@ -4265,6 +4732,28 @@ const COMMAND_TYPES = new Set([
4265
4732
  };
4266
4733
  }
4267
4734
  /** The message routes the app's dispatcher reads, by the app's registry, so its help and dispatch read one table. */ const dispatchedRoutes = new WeakMap();
4735
+ /** What every entry has: its class and method, and the metadata on them. */ function entryBase(controller, method) {
4736
+ const context = new controller_decorator.HandlerExecutionContext({
4737
+ controller,
4738
+ methodName: method,
4739
+ args: []
4740
+ });
4741
+ return {
4742
+ controller,
4743
+ method,
4744
+ get: context.get.bind(context),
4745
+ getAll: context.getAll.bind(context)
4746
+ };
4747
+ }
4748
+ const lateEventSources = new WeakMap();
4749
+ // The late sources each registry's cached entries were read with, so a list made before they arrived is read again
4750
+ const collectedWith = new WeakMap();
4751
+ /**
4752
+ * Has `registry` list the `@On` and `@Once` handlers that run on `sources`, the instances the app attaches them for,
4753
+ * where they are on a class it does not list already.
4754
+ */ function listEventSources(registry, sources) {
4755
+ lateEventSources.set(registry, sources);
4756
+ }
4268
4757
  /** Has `registry` work out message help from `routes`, the table the app's dispatcher routes messages with. */ function shareMessageRoutes(registry, routes) {
4269
4758
  dispatchedRoutes.set(registry, routes);
4270
4759
  }
@@ -4305,7 +4794,11 @@ const COMMAND_TYPES = new Set([
4305
4794
  * @param filter - Narrows the list by kind, controller, or both.
4306
4795
  * @returns The handlers, class by class in the order the app makes its classes, each after what it injects.
4307
4796
  */ list(filter = {}) {
4308
- this.entries ?? (this.entries = this.collect());
4797
+ const late = lateEventSources.get(this);
4798
+ if (!this.entries || collectedWith.get(this) !== late) {
4799
+ this.entries = this.collect();
4800
+ collectedWith.set(this, late);
4801
+ }
4309
4802
  return this.entries.filter((entry)=>(filter.kind === undefined || entry.kind === filter.kind) && (filter.controller === undefined || entry.controller === filter.controller));
4310
4803
  }
4311
4804
  /**
@@ -4329,8 +4822,9 @@ const COMMAND_TYPES = new Set([
4329
4822
  * }
4330
4823
  * ```
4331
4824
  */ async messageHelp(message, query) {
4825
+ // Until a dispatcher shares its table, the same one built from the same controllers, so help lists what dispatch reaches
4332
4826
  const routes = dispatchedRoutes.get(this) ?? this.messageRoutes ?? (this.messageRoutes = buildMessageRoutes([
4333
- ...this.classes
4827
+ ...this.controllers
4334
4828
  ], this.messages));
4335
4829
  const starts = await messageStarts(this.messages, message, message.client?.user?.id);
4336
4830
  const text = (message.content ?? '').trim();
@@ -4360,19 +4854,7 @@ const COMMAND_TYPES = new Set([
4360
4854
  const entries = [];
4361
4855
  for (const controller of this.classes){
4362
4856
  const prototype = controller.prototype;
4363
- const base = (method)=>{
4364
- const context = new controller_decorator.HandlerExecutionContext({
4365
- controller,
4366
- methodName: method,
4367
- args: []
4368
- });
4369
- return {
4370
- controller,
4371
- method,
4372
- get: context.get.bind(context),
4373
- getAll: context.getAll.bind(context)
4374
- };
4375
- };
4857
+ const base = (method)=>entryBase(controller, method);
4376
4858
  for (const [name, metas] of Object.entries(controller_decorator.getCommandMap(prototype) ?? {})){
4377
4859
  for (const { methodName, type, builder } of metas){
4378
4860
  if (COMMAND_TYPES.has(type)) {
@@ -4435,34 +4917,35 @@ const COMMAND_TYPES = new Set([
4435
4917
  });
4436
4918
  }
4437
4919
  }
4920
+ // The classes the app reaches only as something it makes, listed once each, with the handlers that run on them
4921
+ const listed = new Set(this.classes);
4922
+ for (const { cls, handlers } of lateEventSources.get(this) ?? []){
4923
+ if (listed.has(cls)) continue;
4924
+ listed.add(cls);
4925
+ for (const { event, method, once } of handlers)entries.push({
4926
+ ...entryBase(cls, method),
4927
+ kind: 'event',
4928
+ name: event,
4929
+ once
4930
+ });
4931
+ }
4438
4932
  return entries;
4439
4933
  }
4440
4934
  /**
4441
- * @param classes - The app's classes to read handlers from. The factory fills the list
4442
- * once the app is bound; entries are read on the first {@link list}.
4935
+ * @param classes - The app's classes to read handlers from, controllers and services alike. The factory fills the
4936
+ * list once the app is bound; entries are read on the first {@link list}.
4443
4937
  * @param messages - The app's `messages` options, whose `caseSensitive` message entries' `matches` follows.
4444
4938
  * @param translator - The app's translator, read when {@link messageHelp} words its labels; none for English.
4445
- */ constructor(classes, messages = {}, translator = ()=>undefined){
4939
+ * @param controllers - The app's controllers, the classes dispatch routes messages to, which {@link messageHelp}
4940
+ * lists; `classes` when not given.
4941
+ */ constructor(classes, messages = {}, translator = ()=>undefined, controllers = classes){
4446
4942
  this.classes = classes;
4447
4943
  this.messages = messages;
4448
4944
  this.translator = translator;
4945
+ this.controllers = controllers;
4449
4946
  }
4450
4947
  }
4451
4948
 
4452
- /** The longest delay a timer keeps: 2^31 - 1 ms, about 24.8 days. Node fires a longer one at once. */ const MAX_TIMER_MS = 2147483647;
4453
- /** A value as a refusal quotes it: a number as itself, `Infinity` and `NaN` included, a string in quotes. */ function quoted(value) {
4454
- if (typeof value === 'string') return JSON.stringify(value);
4455
- if (typeof value === 'number' || typeof value === 'boolean' || value === null) return String(value);
4456
- return Array.isArray(value) ? 'an array' : typeof value;
4457
- }
4458
- /**
4459
- * What is wrong with a timeout option, or `undefined` when it is a number of milliseconds a timer keeps: finite, above
4460
- * 0, or 0 or more with `allowZero`, and at most `max`, {@link MAX_TIMER_MS} unless less. The caller names the option.
4461
- */ function timeoutProblem(value, { allowZero = false, max = MAX_TIMER_MS } = {}) {
4462
- if (typeof value === 'number' && Number.isFinite(value) && (allowZero ? value >= 0 : value > 0) && value <= max) return undefined;
4463
- return `must be a number of milliseconds ${allowZero ? '0 or more' : 'above 0'}, at most ${max} (got ${quoted(value)})`;
4464
- }
4465
-
4466
4949
  /** Where each shard's client keeps the function that runs a `ShardContext.call` in that shard. */ const SHARD_CALL_KEY = Symbol.for('meocord.shardCall');
4467
4950
  const packArgs = (args)=>Object.assign({
4468
4951
  length: args.length
@@ -4570,11 +5053,11 @@ async function withTimeout(promise, ms, what) {
4570
5053
  *
4571
5054
  * With process sharding that is one result per shard; with one process, a single result listing every
4572
5055
  * shard. The arguments and the result pass as JSON in every mode, one process and tests included, so a
4573
- * `Date` arrives as a string and a `Map` as `{}` wherever it runs, and a value JSON cannot carry, such as a
4574
- * `BigInt`, fails the call. A method whose params JSON would change, such as one taking a `Date`, cannot be called:
4575
- * the argument is refused, naming what to declare instead. An `undefined` argument arrives as `undefined`. A process
4576
- * that throws, lacks the service, or does not answer within 10 seconds gives an error result instead of failing the
4577
- * others.
5056
+ * `Date` arrives as a string, a `Map` as `{}` and a class instance as its own fields, without its getters, wherever
5057
+ * it runs, and a value JSON cannot carry, such as a `BigInt`, fails the call. A method whose params JSON would
5058
+ * change, such as one taking a `Date`, cannot be called: the argument is refused, naming what to declare instead. An
5059
+ * `undefined` argument arrives as `undefined`. A process that throws, lacks the service, or does not answer within
5060
+ * 10 seconds gives an error result instead of failing the others.
4578
5061
  *
4579
5062
  * @param service - The controller, the service, or a class a provider stands in for; each process resolves its own
4580
5063
  * instance.
@@ -4680,6 +5163,18 @@ async function withTimeout(promise, ms, what) {
4680
5163
  controller_decorator.ThemeCache,
4681
5164
  controller_decorator.Translator
4682
5165
  ];
5166
+ /**
5167
+ * The tokens the bot binds before an app's providers, which `@MeoCord({ providers })` cannot provide: `Translator` too
5168
+ * when the app gives `i18n`.
5169
+ */ const boundBeforeProviders = (i18n)=>[
5170
+ discord_js.Client,
5171
+ HandlerRegistry,
5172
+ ShardContext,
5173
+ controller_decorator.CooldownStore,
5174
+ ...i18n ? [
5175
+ controller_decorator.Translator
5176
+ ] : []
5177
+ ];
4683
5178
  const created = (cls)=>`create it with \`new ${cls.name}(…)\``;
4684
5179
  /** What to do in place of injecting each of MeoCord's classes an app makes itself; the testing module adds its own. */ const advice = new Map([
4685
5180
  [
@@ -4718,6 +5213,18 @@ const isFactoryProvider = (provider)=>'useFactory' in provider;
4718
5213
  return `'${String(token)}'`;
4719
5214
  }
4720
5215
  const isToken = (value)=>typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
5216
+ /**
5217
+ * Checks one of an app's own `@MeoCord({ providers })` as the bot does. A provider of a token MeoCord binds later, or
5218
+ * for each call, is warned about until 5.0 refuses it; one of a token `isOwn` names, which MeoCord binds before the
5219
+ * providers, is refused.
5220
+ *
5221
+ * @returns The refusal, for the caller to report, or `undefined`.
5222
+ */ function appProviderRefusal(appName, token, isOwn, logger) {
5223
+ if (token === controller_decorator.ThemeCache || token === controller_decorator.ExecutionContext) {
5224
+ controller_decorator.warnOnce(logger, `${appName}: @MeoCord({ providers }) provides ${tokenName(token)}, which MeoCord binds itself; ` + 'the next major version (5.0) refuses it. Remove the provider.');
5225
+ }
5226
+ return isOwn(token) ? new Error(`${appName}: @MeoCord({ providers }) cannot provide ${tokenName(token)}, which MeoCord binds itself.`) : undefined;
5227
+ }
4721
5228
  /**
4722
5229
  * Checks each provider's shape and indexes them by token, reporting each that cannot be bound, such as one with no
4723
5230
  * token, not exactly one of `useValue`, `useClass` and `useFactory`, one that injects `ExecutionContext`, or a token
@@ -4747,7 +5254,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4747
5254
  controller_decorator.startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
4748
5255
  continue;
4749
5256
  }
4750
- if ('useClass' in entry && typeof entry.useClass !== 'function') {
5257
+ if ('useClass' in entry && !controller_decorator.isConstructor(entry.useClass)) {
4751
5258
  controller_decorator.startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
4752
5259
  continue;
4753
5260
  }
@@ -4777,6 +5284,10 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4777
5284
  }
4778
5285
  return map;
4779
5286
  }
5287
+ /** For each container, how to forget the value each factory provider made, so a start that failed is made afresh. */ const madeValues = new WeakMap();
5288
+ /** Forgets every value `container`'s factory providers made, so the next resolve runs each factory again. */ function forgetFactoryValues(container) {
5289
+ for (const forget of madeValues.get(container) ?? [])forget();
5290
+ }
4780
5291
  /**
4781
5292
  * Binds one provider into `container` as a singleton. `bindClass` binds a class a provider depends
4782
5293
  * on, as the app binds its own, so a provided class or a factory's `inject` can name any class.
@@ -4797,6 +5308,10 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4797
5308
  // Made once, as a singleton, but kept only once made: a factory that throws or rejects runs again on the next
4798
5309
  // resolve, so a start() after it failed tries it again. Resolves while one is pending share it.
4799
5310
  let kept;
5311
+ madeValues.set(container, [
5312
+ ...madeValues.get(container) ?? [],
5313
+ ()=>kept = undefined
5314
+ ]);
4800
5315
  container.bind(token).toDynamicValue((context)=>{
4801
5316
  if (kept) return kept.value;
4802
5317
  // Resolved in dependency order before anything needs it, so each injected value is already made
@@ -4824,6 +5339,15 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4824
5339
  });
4825
5340
  for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
4826
5341
  }
5342
+ /**
5343
+ * Reports each service listed by a string or symbol token that no provider provides, which would otherwise be bound
5344
+ * as if it were a class.
5345
+ */ function assertProvidedServices(appName, services, providers, where) {
5346
+ for (const service of services){
5347
+ if (typeof service === 'function' || providers.has(service)) continue;
5348
+ controller_decorator.startupError(new Error(`${appName}: @MeoCord({ services }) lists ${tokenName(service)}, which nothing provides: add a provider for it to ${where}.`));
5349
+ }
5350
+ }
4827
5351
  /**
4828
5352
  * Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
4829
5353
  * when the class is first resolved. A class token is bound on demand, so it is never missing.
@@ -4959,11 +5483,13 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4959
5483
  * Runs every factory in `order`, awaiting those that return a promise, so each value is made before
4960
5484
  * anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails. With `control`,
4961
5485
  * it makes no further value once the app is stopping.
5486
+ *
5487
+ * @returns Whether every factory was made: `false` when the app began stopping first.
4962
5488
  */ async function resolveProviders(container, providers, order, control) {
4963
5489
  for (const token of order){
4964
5490
  const provider = providers.get(token);
4965
5491
  if (!provider || !isFactoryProvider(provider)) continue;
4966
- if (control?.stopped()) return;
5492
+ if (control?.stopped()) return false;
4967
5493
  control?.pending(tokenName(token));
4968
5494
  try {
4969
5495
  await container.getAsync(token);
@@ -4973,12 +5499,14 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
4973
5499
  control?.pending(undefined);
4974
5500
  }
4975
5501
  }
5502
+ return true;
4976
5503
  }
4977
5504
 
4978
5505
  exports.BUILT_IN_TYPES = BUILT_IN_TYPES;
4979
5506
  exports.COOLDOWN_POLICY = COOLDOWN_POLICY;
4980
5507
  exports.HandlerRegistry = HandlerRegistry;
4981
5508
  exports.MAX_TIMER_MS = MAX_TIMER_MS;
5509
+ exports.MAX_TIMER_SECONDS = MAX_TIMER_SECONDS;
4982
5510
  exports.On = On;
4983
5511
  exports.Once = Once;
4984
5512
  exports.SENT_AS = SENT_AS;
@@ -4988,9 +5516,14 @@ exports.THEMED_CLASSES = THEMED_CLASSES;
4988
5516
  exports.adviseInPlaceOfInjecting = adviseInPlaceOfInjecting;
4989
5517
  exports.appObservers = appObservers;
4990
5518
  exports.appPresenterOf = appPresenterOf;
5519
+ exports.appProviderRefusal = appProviderRefusal;
4991
5520
  exports.appStages = appStages;
5521
+ exports.appTheme = appTheme;
5522
+ exports.appliesTo = appliesTo;
5523
+ exports.applyGuards = applyGuards;
4992
5524
  exports.assertObservers = assertObservers;
4993
5525
  exports.assertProvided = assertProvided;
5526
+ exports.assertProvidedServices = assertProvidedServices;
4994
5527
  exports.assertStageEntries = assertStageEntries;
4995
5528
  exports.assertStartupChecked = assertStartupChecked;
4996
5529
  exports.assertTypedParameters = assertTypedParameters;
@@ -4999,37 +5532,41 @@ exports.bindGlobalStages = bindGlobalStages;
4999
5532
  exports.bindObservers = bindObservers;
5000
5533
  exports.bindProvider = bindProvider;
5001
5534
  exports.bindsOwnToken = bindsOwnToken;
5535
+ exports.boundBeforeProviders = boundBeforeProviders;
5002
5536
  exports.buildMessageRoutes = buildMessageRoutes;
5003
5537
  exports.claimAmbientAppTheme = claimAmbientAppTheme;
5004
5538
  exports.claimCooldownNotice = claimCooldownNotice;
5005
5539
  exports.claimStoreDownNotice = claimStoreDownNotice;
5006
- exports.classChain = classChain;
5007
5540
  exports.classDecorators = classDecorators;
5008
5541
  exports.classLevelGuards = classLevelGuards;
5009
- exports.classUnits = classUnits;
5010
5542
  exports.commandNameOf = commandNameOf;
5011
5543
  exports.commandWordsOf = commandWordsOf;
5012
5544
  exports.computeMessageHelp = computeMessageHelp;
5013
- exports.consumeDispatchMark = consumeDispatchMark;
5014
5545
  exports.cooldownPolicyFrom = cooldownPolicyFrom;
5015
5546
  exports.declaringPrototype = declaringPrototype;
5016
5547
  exports.deferMisuseError = deferMisuseError;
5017
5548
  exports.defineStageTypes = defineStageTypes;
5018
5549
  exports.fitsScope = fitsScope;
5550
+ exports.forgetFactoryValues = forgetFactoryValues;
5019
5551
  exports.getEventHandlers = getEventHandlers;
5020
5552
  exports.globalStagesOf = globalStagesOf;
5553
+ exports.guardOwnHandlersWithBaseGuards = guardOwnHandlersWithBaseGuards;
5021
5554
  exports.handleUnroutedError = handleUnroutedError;
5022
5555
  exports.handlerCooldowns = handlerCooldowns;
5023
5556
  exports.handlerMethods = handlerMethods$1;
5024
5557
  exports.handlerStageClasses = handlerStageClasses;
5025
5558
  exports.handlerStages = handlerStages;
5559
+ exports.hasMessageText = hasMessageText;
5026
5560
  exports.helpInvocation = helpInvocation;
5027
5561
  exports.helpWords = helpWords;
5028
5562
  exports.injectedTokens = injectedTokens;
5029
5563
  exports.isAppClassToken = isAppClassToken;
5030
5564
  exports.isClassProvider = isClassProvider;
5031
5565
  exports.isGuardWithParams = isGuardWithParams;
5566
+ exports.isGuildType = isGuildType;
5032
5567
  exports.isListable = isListable;
5568
+ exports.isValueProvider = isValueProvider;
5569
+ exports.listEventSources = listEventSources;
5033
5570
  exports.makeInjectable = makeInjectable;
5034
5571
  exports.markStartupChecked = markStartupChecked;
5035
5572
  exports.matchHelpRequest = matchHelpRequest;
@@ -5039,17 +5576,21 @@ exports.matchMessageSubcommands = matchMessageSubcommands;
5039
5576
  exports.meocordClasses = meocordClasses;
5040
5577
  exports.messageCommandHooks = messageCommandHooks;
5041
5578
  exports.messageParamsFor = messageParamsFor;
5579
+ exports.messageRouteWarnings = messageRouteWarnings;
5042
5580
  exports.messageStarts = messageStarts;
5043
5581
  exports.messageStartsFor = messageStartsFor;
5044
5582
  exports.needsGuild = needsGuild;
5045
5583
  exports.nonInteractionHandler = nonInteractionHandler;
5046
5584
  exports.observeUnclaimed = observeUnclaimed;
5047
5585
  exports.prepareAppPresenter = prepareAppPresenter;
5586
+ exports.prepareDirectCalls = prepareDirectCalls;
5587
+ exports.prepareEventHandler = prepareEventHandler;
5048
5588
  exports.prepareHandlerStages = prepareHandlerStages;
5589
+ exports.prepareLateThemes = prepareLateThemes;
5049
5590
  exports.prepareThemes = prepareThemes;
5050
5591
  exports.providerMap = providerMap;
5051
5592
  exports.reachableClasses = reachableClasses;
5052
- exports.recordInstanceContainers = recordInstanceContainers;
5593
+ exports.recordGuards = recordGuards;
5053
5594
  exports.registerClientTheme = registerClientTheme;
5054
5595
  exports.registerCommands = registerCommands;
5055
5596
  exports.registrationKey = registrationKey;
@@ -5057,7 +5598,6 @@ exports.releaseAmbientAppTheme = releaseAmbientAppTheme;
5057
5598
  exports.renderMessageHelp = renderMessageHelp;
5058
5599
  exports.resolutionOrder = resolutionOrder;
5059
5600
  exports.resolveProviders = resolveProviders;
5060
- exports.runDirectCall = runDirectCall;
5061
5601
  exports.runGuards = runGuards;
5062
5602
  exports.runHandler = runHandler;
5063
5603
  exports.runInAppTheme = runInAppTheme;
@@ -5071,7 +5611,9 @@ exports.stageTypes = stageTypes;
5071
5611
  exports.staticMessageStarts = staticMessageStarts;
5072
5612
  exports.storeOperationsSettled = storeOperationsSettled;
5073
5613
  exports.subcommandUsageError = subcommandUsageError;
5614
+ exports.takesGuildParam = takesGuildParam;
5074
5615
  exports.timeoutProblem = timeoutProblem;
5616
+ exports.timerKeepsSeconds = timerKeepsSeconds;
5075
5617
  exports.tokenDependencies = tokenDependencies;
5076
5618
  exports.tokenName = tokenName;
5077
5619
  exports.usesAppPrefix = usesAppPrefix;