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
@@ -10,6 +10,7 @@ import { GuardDeniedError } from '../common/errors.js';
10
10
  import { getCommandMap, getMessageHandlers, getReactionHandlers, getAutocompleteHandlers } from '../decorator/controller.decorator.js';
11
11
  import { getEventHandlers } from '../decorator/event.decorator.js';
12
12
  import { META } from '../util/metadata-keys.js';
13
+ import { wrapsMethod } from '../util/method-wrapper.util.js';
13
14
 
14
15
  /** Whether a `@UseGuard` entry is a guard with params rather than a guard class. */ function isGuardWithParams(guard) {
15
16
  // Entries are checked when @UseGuard or @MeoCord applies, so an object here is always { provide, params? }
@@ -259,6 +260,28 @@ const warnedShared = new WeakMap();
259
260
  }
260
261
  return undefined;
261
262
  }
263
+ /**
264
+ * The prototypes on the chain that declare `methodName` as written, base first: the handler and each override of it,
265
+ * looking through methods a class-level `@UseGuard` re-declared to wrap an inherited handler. A handler's method-level
266
+ * stages are read from each, so an override keeps its base method's, as it keeps its route.
267
+ */ function stageOwners(prototype, methodName) {
268
+ const owners = [];
269
+ for(let current = prototype; current && current !== Object.prototype; current = Object.getPrototypeOf(current)){
270
+ if (!Object.prototype.hasOwnProperty.call(current, methodName)) continue;
271
+ if (!Reflect.getOwnMetadata(META.inheritedFrom, current, methodName)) owners.push(current);
272
+ }
273
+ return owners.reverse();
274
+ }
275
+ /** The entries a method-level stage lists under `key` on each of a handler's {@link stageOwners}, base first. */ function methodStages(prototype, methodName, key) {
276
+ return stageOwners(prototype, methodName).flatMap((owner)=>Reflect.getOwnMetadata(key, owner, methodName) ?? []);
277
+ }
278
+ /** 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) {
279
+ for (const owner of stageOwners(prototype, methodName).reverse()){
280
+ const value = Reflect.getOwnMetadata(key, owner, methodName);
281
+ if (value !== undefined) return value;
282
+ }
283
+ return undefined;
284
+ }
262
285
  /**
263
286
  * Caches a function of a class's prototype and one of its handlers. Handler metadata is fixed once
264
287
  * the classes are decorated, so dispatch resolves each handler's stages once, not on every call.
@@ -310,50 +333,165 @@ const warnedShared = new WeakMap();
310
333
  * The guards dispatch runs before a handler: its classes' class-level guards, outermost first, then
311
334
  * the method's. A method that is no handler has only its own, since class guards apply to handlers.
312
335
  */ const handlerGuards = perHandler((prototype, methodName)=>{
313
- const source = sourcePrototype(prototype, methodName);
314
- if (!source) return [];
336
+ if (!sourcePrototype(prototype, methodName)) return [];
315
337
  return [
316
338
  ...handlerMethods(prototype).has(methodName) ? stageClasses(prototype, methodName).flatMap(classLevelGuards) : [],
317
- ...Reflect.getOwnMetadata(META.methodGuards, source, methodName) ?? []
339
+ ...methodStages(prototype, methodName, META.methodGuards)
318
340
  ];
319
341
  });
320
- const wrapperCount = perHandler((prototype, methodName)=>{
321
- const owner = declaringPrototype(prototype, methodName);
322
- return owner ? Reflect.getOwnMetadata(META.guardWrappers, owner, methodName) ?? 0 : 0;
342
+ /**
343
+ * The type a direct call to a handler runs as, which is the one dispatch gives it: `'event'` for an `@On` or `@Once`
344
+ * handler, as its first argument may be a message or an interaction; otherwise read from the first argument.
345
+ */ const directCallType = perHandler((prototype, methodName)=>{
346
+ if (!getEventHandlers(prototype).some((handler)=>handler.method === methodName)) return undefined;
347
+ const routed = [
348
+ ...Object.values(getCommandMap(prototype) ?? {}).flat().map((command)=>command.methodName),
349
+ ...getMessageHandlers(prototype).map((handler)=>handler.method),
350
+ ...getReactionHandlers(prototype).map((handler)=>handler.method),
351
+ ...getAutocompleteHandlers(prototype).map((handler)=>handler.methodName)
352
+ ];
353
+ // A method dispatch also routes as a command, message or reaction has no one type
354
+ return routed.includes(methodName) ? undefined : 'event';
355
+ });
356
+ /** Whether `@UseGuard` wraps the method anywhere on the chain, an override's base included. */ const isWrapped = perHandler((prototype, methodName)=>{
357
+ for(let current = prototype; current; current = Object.getPrototypeOf(current)){
358
+ if (Object.prototype.hasOwnProperty.call(current, methodName) && Reflect.getOwnMetadata(META.guardWrappers, current, methodName)) return true;
359
+ }
360
+ return false;
323
361
  });
324
362
  /**
325
- * Calls dispatch has already guarded, keyed by first argument and method, counting the wrappers still
326
- * to pass. A user decorator that awaits between two wrappers leaves a pass pending during that await,
327
- * which a concurrent direct call with the same object and method could take; other overlaps fail closed.
328
- * An event whose first argument is not an object, such as `debug`'s string, is keyed by the instance.
329
- */ const dispatched = new WeakMap();
330
- /** 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) {
331
- return typeof first === 'object' && first !== null ? first : instance;
332
- }
333
- /** Takes one pass for the wrapper being entered, if dispatch left one for this call. */ function consumeDispatchMark(first, instance, methodName) {
334
- const marks = dispatched.get(markKey(first, instance));
335
- const remaining = marks?.get(methodName);
336
- if (!marks || !remaining) return false;
337
- if (remaining > 1) marks.set(methodName, remaining - 1);
338
- else marks.delete(methodName);
363
+ * The pass of the guarded call running. Kept per call, so a concurrent call with the same object and method runs its own
364
+ * guards, and closed when the call returns, so work it leaves running, such as a timer, does not carry it.
365
+ */ const guardedPasses = new AsyncLocalStorage();
366
+ /** 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) {
367
+ const pass = guardedPasses.getStore();
368
+ if (!pass?.open || pass.instance !== instance || pass.methodName !== methodName || pass.first !== first) return false;
369
+ if (pass.entered.has(wrapper)) return false;
370
+ pass.entered.add(wrapper);
339
371
  return true;
340
372
  }
373
+ /** 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) {
374
+ const pass = {
375
+ instance,
376
+ methodName,
377
+ first: args[0],
378
+ entered: new Set(wrapper ? [
379
+ wrapper
380
+ ] : []),
381
+ open: true
382
+ };
383
+ try {
384
+ return await guardedPasses.run(pass, call);
385
+ } finally{
386
+ pass.open = false;
387
+ }
388
+ }
389
+ /**
390
+ * Adds guards to a method's class or method list, then republishes the effective list under
391
+ * `META.guards`, in the order they run: the bases' before the class's own, and within one class or method the
392
+ * latest decorator first, since it wraps outermost.
393
+ */ function recordGuards(key, guards, prototype, methodName) {
394
+ const existing = Reflect.getOwnMetadata(key, prototype, methodName) ?? [];
395
+ const bases = key === META.classGuards ? Reflect.getOwnMetadata(META.baseClassGuards, prototype, methodName) ?? 0 : 0;
396
+ Reflect.defineMetadata(key, [
397
+ ...existing.slice(0, bases),
398
+ ...guards,
399
+ ...existing.slice(bases)
400
+ ], prototype, methodName);
401
+ const [classGuards, inheritedGuards] = [
402
+ META.classGuards,
403
+ META.inheritedGuards
404
+ ].map((list)=>Reflect.getOwnMetadata(list, prototype, methodName) ?? []);
405
+ // The method's guards and those of the methods it overrides, base first, as dispatch runs them
406
+ const methodGuards = methodStages(prototype, methodName, META.methodGuards);
407
+ Reflect.defineMetadata(META.guards, [
408
+ ...inheritedGuards,
409
+ ...classGuards,
410
+ ...methodGuards
411
+ ], prototype, methodName);
412
+ }
413
+ /** The classes {@link guardOwnHandlersWithBaseGuards} has handled, as `@Controller` and each app's startup both call it. */ const guardedWithBases = new WeakSet();
414
+ /**
415
+ * Gives each handler a class declares itself the class-level guards of the classes it extends,
416
+ * as its inherited handlers have: recorded under `META.guards`, and wrapped so a direct call
417
+ * runs them when no other guard wraps the handler. `@Controller` calls this as it decorates, and
418
+ * {@link prepareDirectCalls} for every class an app runs, a service included. Once per class.
419
+ */ function guardOwnHandlersWithBaseGuards(target) {
420
+ if (guardedWithBases.has(target)) return;
421
+ guardedWithBases.add(target);
422
+ const prototype = target.prototype;
423
+ for (const methodName of handlerMethods(prototype)){
424
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
425
+ // Inherited handlers have the chain of the class that declares them
426
+ if (typeof descriptor?.value !== 'function' || Reflect.getOwnMetadata(META.inheritedFrom, prototype, methodName)) continue;
427
+ // Base first, as dispatch runs them
428
+ const guards = classChain(prototype, prototype).slice(1).reverse().flatMap(classLevelGuards);
429
+ if (guards.length === 0) continue;
430
+ if (!Reflect.getOwnMetadata(META.guardWrappers, prototype, methodName)) {
431
+ applyGuards(descriptor, prototype, methodName);
432
+ Object.defineProperty(prototype, methodName, descriptor);
433
+ }
434
+ recordGuards(META.inheritedGuards, guards, prototype, methodName);
435
+ }
436
+ }
341
437
  /** The container that made each instance, so a direct call runs in its own app or testing module. */ const instanceContainers = new WeakMap();
342
438
  /**
343
- * Records each instance `container` makes of `classes` as its own. Two testing modules made from one class each run a
344
- * direct call on their own instance in themselves, where the class's metadata names only the last.
345
- */ function recordInstanceContainers(container, classes) {
439
+ * Readies direct calls to the handlers of the classes an app or testing module runs, controllers and services alike:
440
+ * - stamps each class with the container, and records each instance the container makes as its own, so two testing
441
+ * modules made from one class each run a direct call on their own instance in themselves;
442
+ * - gives each handler a class declares itself the class-level guards of the classes it extends, and wraps each
443
+ * override of a guarded handler, so a direct call to it runs the guards it inherits before its body.
444
+ */ function prepareDirectCalls(container, classes) {
346
445
  for (const cls of classes){
446
+ Reflect.defineMetadata(META.container, container, cls);
447
+ // Each class on the chain, base first, so an override above the one the app runs is wrapped too
448
+ for (const owner of classesOf(cls).reverse())guardOwnHandlersWithBaseGuards(owner);
449
+ wrapGuardedOverrides(cls.prototype);
347
450
  container.onActivation(cls, (_context, instance)=>{
348
451
  if (typeof instance === 'object' && instance !== null) instanceContainers.set(instance, container);
349
452
  return instance;
350
453
  });
351
454
  }
352
455
  }
456
+ /** `cls` and the classes it extends, innermost first. */ function classesOf(cls) {
457
+ const classes = [];
458
+ for(let current = cls.prototype; current && current !== Object.prototype; current = Object.getPrototypeOf(current)){
459
+ classes.push(current.constructor);
460
+ }
461
+ return classes;
462
+ }
463
+ /** Wraps each method on the chain that overrides a handler a base method guards and is not wrapped itself. */ function wrapGuardedOverrides(prototype) {
464
+ for (const methodName of handlerMethods(prototype)){
465
+ let guardedBelow = false;
466
+ for (const owner of stageOwners(prototype, methodName)){
467
+ const descriptor = Object.getOwnPropertyDescriptor(owner, methodName);
468
+ if (guardedBelow && typeof descriptor?.value === 'function' && !Reflect.getOwnMetadata(META.guardWrappers, owner, methodName)) {
469
+ applyGuards(descriptor, owner, methodName);
470
+ Object.defineProperty(owner, methodName, descriptor);
471
+ }
472
+ if ((Reflect.getOwnMetadata(META.methodGuards, owner, methodName) ?? []).length > 0) guardedBelow = true;
473
+ }
474
+ }
475
+ }
476
+ /**
477
+ * Wraps a method so a direct call runs its guards first: the wrapper entered first runs the whole
478
+ * chain dispatch resolves for the handler, and lets the ones inside it through. A call from dispatch,
479
+ * which has already run the guards, passes through every wrapper.
480
+ */ function applyGuards(descriptor, prototype, propertyKey) {
481
+ const originalMethod = descriptor.value;
482
+ // Which wrapper a call has passed, so it passes each once
483
+ const wrapper = {};
484
+ descriptor.value = wrapsMethod(async function(...args) {
485
+ if (takeGuardedPass(wrapper, args[0], this, propertyKey)) return originalMethod.apply(this, args);
486
+ return runDirectCall(this, propertyKey, args, ()=>originalMethod.apply(this, args), wrapper);
487
+ }, originalMethod);
488
+ const wrappers = Reflect.getOwnMetadata(META.guardWrappers, prototype, propertyKey) ?? 0;
489
+ Reflect.defineMetadata(META.guardWrappers, wrappers + 1, prototype, propertyKey);
490
+ }
353
491
  /**
354
- * Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves them, then the
355
- * call with a pass for each wrapper inside the one entered, so no guard runs twice.
356
- */ async function runDirectCall(instance, methodName, args, call) {
492
+ * Runs a direct call to a guarded handler: the handler's guards, as dispatch resolves and types them, then
493
+ * the call with a pass every wrapper inside the one entered takes, so no guard runs twice.
494
+ */ async function runDirectCall(instance, methodName, args, call, wrapper) {
357
495
  const prototype = Object.getPrototypeOf(instance);
358
496
  const controller = instance.constructor;
359
497
  // The container that made the instance, or else the app that made the class or a class it extends: a subclass the
@@ -362,39 +500,22 @@ const wrapperCount = perHandler((prototype, methodName)=>{
362
500
  if (!container) {
363
501
  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.`);
364
502
  }
503
+ const type = directCallType(prototype, methodName);
365
504
  if (!await runGuards(handlerGuards(prototype, methodName), {
366
505
  container,
367
506
  controller,
368
507
  methodName,
369
- args
508
+ args,
509
+ type
370
510
  })) return undefined;
371
- const inner = wrapperCount(prototype, methodName) - 1;
372
- if (inner <= 0) return call();
373
- const key = markKey(args[0], instance);
374
- let marks = dispatched.get(key);
375
- if (!marks) dispatched.set(key, marks = new Map());
376
- marks.set(methodName, inner);
377
- try {
378
- return await call();
379
- } finally{
380
- marks.delete(methodName);
381
- }
511
+ return runWithPass(instance, methodName, args, call, wrapper);
382
512
  }
383
513
  /**
384
- * Calls a handler dispatch has already guarded. Its wrappers let this one call through; the passes
385
- * are removed afterwards, so a later direct call with the same (cached) object runs its guards.
514
+ * Calls a handler dispatch has already guarded. Its wrappers let this one call through; the pass
515
+ * closes afterwards, so a later direct call with the same (cached) object runs its guards.
386
516
  */ async function callGuardedHandler(instance, methodName, args) {
387
- const count = wrapperCount(Object.getPrototypeOf(instance), methodName);
388
- if (count === 0) return instance[methodName](...args);
389
- const key = markKey(args[0], instance);
390
- let marks = dispatched.get(key);
391
- if (!marks) dispatched.set(key, marks = new Map());
392
- marks.set(methodName, count);
393
- try {
394
- return await instance[methodName](...args);
395
- } finally{
396
- marks.delete(methodName);
397
- }
517
+ if (!isWrapped(Object.getPrototypeOf(instance), methodName)) return instance[methodName](...args);
518
+ return runWithPass(instance, methodName, args, ()=>instance[methodName](...args));
398
519
  }
399
520
 
400
- export { callGuardedHandler, classChain, classLevelGuards, consumeDispatchMark, declaringPrototype, handlerGuards, handlerMethods, injectedTokens, isGuardWithParams, perHandler, recordInstanceContainers, runDirectCall, runGuards, singletonContextError, sourcePrototype, stageClasses, undecoratedConstructor, untypedParameter };
521
+ export { applyGuards, callGuardedHandler, classChain, classLevelGuards, declaringPrototype, guardOwnHandlersWithBaseGuards, handlerGuards, handlerMethods, injectedTokens, isGuardWithParams, methodStages, nearestMethodStage, perHandler, prepareDirectCalls, recordGuards, runDirectCall, runGuards, singletonContextError, sourcePrototype, stageClasses, stageOwners, takeGuardedPass, undecoratedConstructor, untypedParameter };
@@ -1,9 +1,10 @@
1
1
  import { ModalSubmitInteraction, ComponentType, StringSelectMenuInteraction, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, MentionableSelectMenuInteraction } from 'discord.js';
2
- import { getCommandMap } from '../decorator/controller.decorator.js';
3
2
  import { matchComponentRoute } from './component-routes.js';
4
- import { hasCustomId, isCustomIdRouted, matchesHandler, resolveOptionParams, matchesCommandType } from '../util/interaction.util.js';
3
+ import { hasCustomId, isCustomIdRouted, matchesHandler, resolveOptionParams, describeInteraction, focusedOptionName, matchesCommandType } from '../util/interaction.util.js';
5
4
  import { setOwn } from '../util/value.util.js';
6
5
  import { commandRouteKeys, matchCommandRoute } from './command-routes.js';
6
+ import { matchAutocompleteRoute, buildAutocompleteRoutes } from './autocomplete-routes.js';
7
+ import { getCommandMap, getAutocompleteHandlers } from '../decorator/controller.decorator.js';
7
8
 
8
9
  /** The entries of a discord.js Collection, or none for anything else, such as a test double's stub. */ const entriesOf = (collection)=>collection instanceof Map ? [
9
10
  ...collection.values()
@@ -143,5 +144,25 @@ import { commandRouteKeys, matchCommandRoute } from './command-routes.js';
143
144
  if (winner) return `command '${keys[0]}' does not reach ${handler}: dispatch runs ${winner.controllerClass.name}.${winner.meta.methodName}.`;
144
145
  return `command '${keys[0]}' does not match ${handler}'s route ${named.map(({ route })=>`'${route}'`).join(' or ')}.`;
145
146
  }
147
+ /**
148
+ * Why an autocomplete could not reach a handler, for a test calling the handler directly: the handler is checked
149
+ * against the one dispatch routes the autocomplete to among `controllers`, by its command path and focused option.
150
+ * `undefined` when it is that one, when the handler has no `@Autocomplete` route, or when the interaction is not an
151
+ * autocomplete or has no command name.
152
+ */ function autocompleteMismatch(controllers, controller, methodName, interaction) {
153
+ if (!interaction.isAutocomplete() || typeof interaction.commandName !== 'string') return undefined;
154
+ const own = getAutocompleteHandlers(controller.prototype).filter((meta)=>meta.methodName === methodName);
155
+ if (own.length === 0) return undefined;
156
+ const winner = matchAutocompleteRoute(buildAutocompleteRoutes(controllers), interaction);
157
+ if (winner?.controllerClass === controller && winner.meta.methodName === methodName) return undefined;
158
+ const handler = `${controller.name}.${methodName}`;
159
+ const received = describeInteraction(interaction);
160
+ // A mock without a focused option reaches no option's handler, the commonest way to miss one
161
+ const option = focusedOptionName(interaction) === undefined ? own.find((meta)=>meta.optionName !== undefined)?.optionName : undefined;
162
+ const fix = option === undefined ? '' : ` Give the mock the focused option: options: createChatInputOptions({ focused: '${option}' }).`;
163
+ if (winner) return `${received} does not reach ${handler}: dispatch runs ${winner.controllerClass.name}.${winner.meta.methodName}.${fix}`;
164
+ const routes = own.map((meta)=>meta.optionName === undefined ? `'${meta.commandPath}'` : `'${meta.commandPath}' option '${meta.optionName}'`);
165
+ return `${received} does not match ${handler}'s route ${routes.join(' or ')}.${fix}`;
166
+ }
146
167
 
147
- export { commandMismatch, componentRouteFor, handlerInput };
168
+ export { autocompleteMismatch, commandMismatch, componentRouteFor, handlerInput };
@@ -1,13 +1,13 @@
1
1
  import 'reflect-metadata';
2
- import { refuse, startupError } from '../util/refusal.util.js';
2
+ import { refuse, startupError, collectStartupErrors, isRefusal, startupErrorsOf } from '../util/refusal.util.js';
3
3
  import { setPresenter } from '../common/response/presenter.js';
4
4
  import { responseOf } from '../common/response/response-state.js';
5
- import { handlerDefer, nonInteractionHandler, deferMisuseError, startDefer } from './defer.js';
6
- import { perHandler, runGuards, callGuardedHandler, handlerGuards, isGuardWithParams } from './guard-runner.js';
7
- import { prepareInterceptor, handlerInterceptors, bindShared, runInterceptors, interceptorClass } from './interceptor-runner.js';
5
+ import { handlerDefer, startDefer, nonInteractionHandler, deferMisuseError } from './defer.js';
6
+ import { perHandler, handlerGuards, runGuards, callGuardedHandler, isGuardWithParams } from './guard-runner.js';
7
+ import { prepareInterceptor, bindShared, interceptorClass, handlerInterceptors, runInterceptors } from './interceptor-runner.js';
8
8
  import { inferContextType, HandlerExecutionContext, UnroutedExecutionContext } from '../common/execution-context.js';
9
- import { prepareFilter, handlerFilterLevels, filterClass, matchFilter, callFilter } from './filter-runner.js';
10
- import { handlerInputStages, preparePipe, prepareHandlerArgs, pipeClass } from './input-runner.js';
9
+ import { prepareFilter, filterClass, handlerFilterLevels, matchFilter, callFilter } from './filter-runner.js';
10
+ import { handlerInputStages, pipeClass, prepareHandlerArgs, preparePipe } from './input-runner.js';
11
11
  import { hasObservers, notifyStart, outcomeOf, responsePhaseOf, notifyObservers } from './observer-runner.js';
12
12
  import { handlerCooldowns, peekCooldowns, methodCooldowns } from './cooldown-runner.js';
13
13
  import { Logger } from '../common/logger.js';
@@ -165,19 +165,35 @@ const stagesByGlobals = new WeakMap();
165
165
  for (const entry of globals.filters)prepareFilter(container, entry);
166
166
  for (const controller of controllers){
167
167
  const prototype = controller.prototype;
168
- for (const method of handlerMethods(prototype)){
169
- const kind = handlerDefer(prototype, method) ? nonInteractionHandler(prototype, method) : undefined;
170
- if (kind) {
171
- startupError(deferMisuseError(controller.name, method, kind));
172
- continue;
173
- }
174
- for (const entry of handlerInterceptors(prototype, method))prepareInterceptor(container, entry);
175
- for (const entry of handlerFilterLevels(prototype, method, []).flat())prepareFilter(container, entry);
176
- for (const { entry } of handlerInputStages(prototype, method).pipes)preparePipe(container, entry);
177
- }
168
+ for (const method of handlerMethods(prototype))prepareHandler(container, controller, method);
178
169
  assertInputStagesOnInteractions(controller, prototype);
179
170
  }
180
171
  }
172
+ /**
173
+ * Prepares one `@On` or `@Once` handler of a class the app reaches only as something it makes, as
174
+ * {@link prepareHandlerStages} prepares a controller's.
175
+ *
176
+ * @returns The errors a controller's handler would be refused with; none when the handler is ready to run.
177
+ */ function prepareEventHandler(container, cls, method) {
178
+ try {
179
+ collectStartupErrors(()=>{
180
+ prepareHandler(container, cls, method);
181
+ assertInputStages(cls, cls.prototype, method, 'event');
182
+ });
183
+ return [];
184
+ } catch (error) {
185
+ if (!isRefusal(error)) throw error;
186
+ return startupErrorsOf(error);
187
+ }
188
+ }
189
+ /** Binds what one handler's interceptors, filters and pipes need, refusing `@Defer` where it cannot apply. */ function prepareHandler(container, controller, method) {
190
+ const prototype = controller.prototype;
191
+ const kind = handlerDefer(prototype, method) ? nonInteractionHandler(prototype, method) : undefined;
192
+ if (kind) return startupError(deferMisuseError(controller.name, method, kind));
193
+ for (const entry of handlerInterceptors(prototype, method))prepareInterceptor(container, entry);
194
+ for (const entry of handlerFilterLevels(prototype, method, []).flat())prepareFilter(container, entry);
195
+ for (const { entry } of handlerInputStages(prototype, method).pipes)preparePipe(container, entry);
196
+ }
181
197
  /** 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) {
182
198
  const globals = globalStagesOf(container);
183
199
  configureThemes(container, globals.theme, controllers, globals.themeFor);
@@ -223,17 +239,17 @@ const stagesByGlobals = new WeakMap();
223
239
  'event'
224
240
  ])
225
241
  ];
226
- for (const [method, kind] of others){
227
- const { schema, pipes } = handlerInputStages(prototype, method);
228
- const handler = kind === 'message' ? 'a message handler without a pattern' : `${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} handler`;
229
- if (schema || pipes.length > 0) {
230
- 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}.`));
231
- continue;
232
- }
233
- // A controller's own @Cooldown skips these handlers; one on the method itself is a mistake.
234
- if (kind !== 'message' && methodCooldowns(prototype, method).length > 0) {
235
- startupError(new Error(`${controller.name}.${method}: @Cooldown is for interaction and message handlers, and this is ${handler}.`));
236
- }
242
+ for (const [method, kind] of others)assertInputStages(controller, prototype, method, kind);
243
+ }
244
+ /** 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) {
245
+ const { schema, pipes } = handlerInputStages(prototype, method);
246
+ const handler = kind === 'message' ? 'a message handler without a pattern' : `${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} handler`;
247
+ if (schema || pipes.length > 0) {
248
+ return 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}.`));
249
+ }
250
+ // A controller's own @Cooldown skips these handlers; one on the method itself is a mistake.
251
+ if (kind !== 'message' && methodCooldowns(prototype, method).length > 0) {
252
+ startupError(new Error(`${controller.name}.${method}: @Cooldown is for interaction and message handlers, and this is ${handler}.`));
237
253
  }
238
254
  }
239
255
  /**
@@ -343,7 +359,9 @@ const logger = new Logger('ExceptionFilter');
343
359
  denial
344
360
  })) {
345
361
  outcome = 'denied';
346
- await response?.abandon();
362
+ // Without @Defer too, so a denied component is acknowledged rather than left to fail
363
+ const denied = response ?? (type === 'interaction' && first?.isRepliable?.() ? responseOf(first) : undefined);
364
+ await denied?.abandon();
347
365
  return {
348
366
  ran: false
349
367
  };
@@ -399,14 +417,18 @@ const logger = new Logger('ExceptionFilter');
399
417
  };
400
418
  } finally{
401
419
  await response?.release();
402
- if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && type === 'interaction') {
420
+ if (options.onUnanswered && (ran || returnedEarly) && outcome === 'ran' && (type === 'interaction' || type === 'autocomplete')) {
403
421
  const phase = responsePhaseOf(contextOf());
404
422
  // The handler is to blame only once it has finished; until then, the interceptor that ended the call is
405
- const blamed = !finished && returnedEarly ? {
423
+ const returnedBy = !finished && returnedEarly ? {
406
424
  interceptor: returnedEarly,
407
425
  handlerStarted: ran
408
426
  } : undefined;
409
- if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered(phase, blamed);
427
+ if (phase === 'unanswered' || phase === 'deferred') options.onUnanswered({
428
+ type,
429
+ phase,
430
+ returnedBy
431
+ });
410
432
  }
411
433
  if (options.awaitObservers) await starting;
412
434
  // After the answer and the release, so the duration covers the whole call
@@ -475,4 +497,4 @@ const logger = new Logger('ExceptionFilter');
475
497
  }, options);
476
498
  }
477
499
 
478
- export { appPresenterOf, appStages, bindAppPresenter, bindGlobalStages, classDecorators, globalStagesOf, handleUnroutedError, handlerStageClasses, handlerStages, observeUnclaimed, prepareAppPresenter, prepareHandlerStages, prepareThemes, runHandler, runInAppTheme };
500
+ export { appPresenterOf, appStages, bindAppPresenter, bindGlobalStages, classDecorators, globalStagesOf, handleUnroutedError, handlerStageClasses, handlerStages, observeUnclaimed, prepareAppPresenter, prepareEventHandler, prepareHandlerStages, prepareThemes, runHandler, runInAppTheme };
@@ -4,10 +4,10 @@ import { getCommandMap, getAutocompleteHandlers, getMessageHandlers, getReaction
4
4
  import { getEventHandlers } from '../decorator/event.decorator.js';
5
5
  import { CommandType } from '../enum/controller.enum.js';
6
6
  import { buildMessageRoutes, messageStarts, afterStart, parseMessagePattern, commandWordsOf } from './message-routes.js';
7
- import { computeMessageHelp, helpInvocation, effectiveScope } from './message-help.js';
7
+ import { computeMessageHelp, helpInvocation } from './message-help.js';
8
+ import { usageOf, effectiveScope } from './message-params.js';
8
9
  import { registrationKey, SENT_AS, commandNameOf, serialise } from './command-registration.js';
9
10
  import { textRenderer, messageLocale } from '../common/meocord-text.js';
10
- import { usageOf } from './message-params.js';
11
11
 
12
12
  const COMMAND_TYPES = new Set([
13
13
  CommandType.SLASH,
@@ -65,6 +65,28 @@ const COMMAND_TYPES = new Set([
65
65
  };
66
66
  }
67
67
  /** 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();
68
+ /** What every entry has: its class and method, and the metadata on them. */ function entryBase(controller, method) {
69
+ const context = new HandlerExecutionContext({
70
+ controller,
71
+ methodName: method,
72
+ args: []
73
+ });
74
+ return {
75
+ controller,
76
+ method,
77
+ get: context.get.bind(context),
78
+ getAll: context.getAll.bind(context)
79
+ };
80
+ }
81
+ const lateEventSources = new WeakMap();
82
+ // The late sources each registry's cached entries were read with, so a list made before they arrived is read again
83
+ const collectedWith = new WeakMap();
84
+ /**
85
+ * Has `registry` list the `@On` and `@Once` handlers that run on `sources`, the instances the app attaches them for,
86
+ * where they are on a class it does not list already.
87
+ */ function listEventSources(registry, sources) {
88
+ lateEventSources.set(registry, sources);
89
+ }
68
90
  /** Has `registry` work out message help from `routes`, the table the app's dispatcher routes messages with. */ function shareMessageRoutes(registry, routes) {
69
91
  dispatchedRoutes.set(registry, routes);
70
92
  }
@@ -105,7 +127,11 @@ const COMMAND_TYPES = new Set([
105
127
  * @param filter - Narrows the list by kind, controller, or both.
106
128
  * @returns The handlers, class by class in the order the app makes its classes, each after what it injects.
107
129
  */ list(filter = {}) {
108
- this.entries ?? (this.entries = this.collect());
130
+ const late = lateEventSources.get(this);
131
+ if (!this.entries || collectedWith.get(this) !== late) {
132
+ this.entries = this.collect();
133
+ collectedWith.set(this, late);
134
+ }
109
135
  return this.entries.filter((entry)=>(filter.kind === undefined || entry.kind === filter.kind) && (filter.controller === undefined || entry.controller === filter.controller));
110
136
  }
111
137
  /**
@@ -129,8 +155,9 @@ const COMMAND_TYPES = new Set([
129
155
  * }
130
156
  * ```
131
157
  */ async messageHelp(message, query) {
158
+ // Until a dispatcher shares its table, the same one built from the same controllers, so help lists what dispatch reaches
132
159
  const routes = dispatchedRoutes.get(this) ?? this.messageRoutes ?? (this.messageRoutes = buildMessageRoutes([
133
- ...this.classes
160
+ ...this.controllers
134
161
  ], this.messages));
135
162
  const starts = await messageStarts(this.messages, message, message.client?.user?.id);
136
163
  const text = (message.content ?? '').trim();
@@ -160,19 +187,7 @@ const COMMAND_TYPES = new Set([
160
187
  const entries = [];
161
188
  for (const controller of this.classes){
162
189
  const prototype = controller.prototype;
163
- const base = (method)=>{
164
- const context = new HandlerExecutionContext({
165
- controller,
166
- methodName: method,
167
- args: []
168
- });
169
- return {
170
- controller,
171
- method,
172
- get: context.get.bind(context),
173
- getAll: context.getAll.bind(context)
174
- };
175
- };
190
+ const base = (method)=>entryBase(controller, method);
176
191
  for (const [name, metas] of Object.entries(getCommandMap(prototype) ?? {})){
177
192
  for (const { methodName, type, builder } of metas){
178
193
  if (COMMAND_TYPES.has(type)) {
@@ -235,18 +250,33 @@ const COMMAND_TYPES = new Set([
235
250
  });
236
251
  }
237
252
  }
253
+ // The classes the app reaches only as something it makes, listed once each, with the handlers that run on them
254
+ const listed = new Set(this.classes);
255
+ for (const { cls, handlers } of lateEventSources.get(this) ?? []){
256
+ if (listed.has(cls)) continue;
257
+ listed.add(cls);
258
+ for (const { event, method, once } of handlers)entries.push({
259
+ ...entryBase(cls, method),
260
+ kind: 'event',
261
+ name: event,
262
+ once
263
+ });
264
+ }
238
265
  return entries;
239
266
  }
240
267
  /**
241
- * @param classes - The app's classes to read handlers from. The factory fills the list
242
- * once the app is bound; entries are read on the first {@link list}.
268
+ * @param classes - The app's classes to read handlers from, controllers and services alike. The factory fills the
269
+ * list once the app is bound; entries are read on the first {@link list}.
243
270
  * @param messages - The app's `messages` options, whose `caseSensitive` message entries' `matches` follows.
244
271
  * @param translator - The app's translator, read when {@link messageHelp} words its labels; none for English.
245
- */ constructor(classes, messages = {}, translator = ()=>undefined){
272
+ * @param controllers - The app's controllers, the classes dispatch routes messages to, which {@link messageHelp}
273
+ * lists; `classes` when not given.
274
+ */ constructor(classes, messages = {}, translator = ()=>undefined, controllers = classes){
246
275
  this.classes = classes;
247
276
  this.messages = messages;
248
277
  this.translator = translator;
278
+ this.controllers = controllers;
249
279
  }
250
280
  }
251
281
 
252
- export { HandlerRegistry, shareMessageRoutes };
282
+ export { HandlerRegistry, listEventSources, shareMessageRoutes };
@@ -1,7 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { ValidationError } from '../common/errors.js';
3
3
  import { prepareInterceptor } from './interceptor-runner.js';
4
- import { perHandler, sourcePrototype } from './guard-runner.js';
4
+ import { perHandler, nearestMethodStage, methodStages } from './guard-runner.js';
5
5
  import { handlerCooldowns, consumeCooldowns } from './cooldown-runner.js';
6
6
  import { META } from '../util/metadata-keys.js';
7
7
 
@@ -15,17 +15,16 @@ function isPipeWithParams(entry) {
15
15
  const asList = (entries)=>Array.isArray(entries) ? entries : [
16
16
  entries
17
17
  ];
18
- /** The validation and pipes that run on a handler's input: `@Validate`'s first, then `@UsePipe`'s. */ const handlerInputStages = perHandler((prototype, methodName)=>{
19
- const source = sourcePrototype(prototype, methodName);
20
- if (!source) return {
21
- pipes: []
22
- };
23
- const validation = Reflect.getOwnMetadata(META.methodValidation, source, methodName);
18
+ /**
19
+ * The validation and pipes that run on a handler's input: `@Validate`'s first, then `@UsePipe`'s. The `@Validate` is the
20
+ * handler's own, or else that of the nearest method it overrides that has one; `@UsePipe`'s are every one's, base first.
21
+ */ const handlerInputStages = perHandler((prototype, methodName)=>{
22
+ const validation = nearestMethodStage(prototype, methodName, META.methodValidation);
24
23
  const inline = Object.entries(validation?.pipes ?? {}).flatMap(([key, entries])=>asList(entries).map((entry)=>({
25
24
  key,
26
25
  entry
27
26
  })));
28
- const used = Reflect.getOwnMetadata(META.methodPipes, source, methodName) ?? [];
27
+ const used = methodStages(prototype, methodName, META.methodPipes);
29
28
  return {
30
29
  schema: validation?.schema,
31
30
  pipes: [