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
@@ -64,50 +64,50 @@ const merges = new WeakMap();
64
64
  if (!merged) byLayer.set(layer, merged = deepFreeze(mergeInto(theme, layer)));
65
65
  return merged;
66
66
  }
67
- // Written by the deprecated `Theme` statics: beneath every theme an app sets
68
- let legacyLayer;
69
- let legacyVersion = 0;
70
- let defaults = DEFAULT_THEME;
67
+ /**
68
+ * The theme state outside a call, one for both builds of this version so either reads what the other set: the defaults
69
+ * with what the deprecated `Theme` statics set beneath every theme, the app whose theme is read outside a call, and how
70
+ * an answer outside a call finds its theme.
71
+ */ const state = shared('themeState', ()=>({
72
+ defaults: DEFAULT_THEME,
73
+ legacyVersion: 0,
74
+ ambient: undefined,
75
+ ambientVersion: 0,
76
+ themeOutsideCalls: undefined
77
+ }));
71
78
  /** MeoCord's defaults, with what the deprecated `Theme` statics set over them. */ function defaultTheme() {
72
- return defaults;
79
+ return state.defaults;
73
80
  }
74
81
  /** The number of times the legacy layer has changed, so a theme built on an older one is built again. */ function themeLayersVersion() {
75
- return legacyVersion;
82
+ return state.legacyVersion;
76
83
  }
77
84
  /** Sets the layer the deprecated `Theme` statics write, merged over MeoCord's defaults. */ function setLegacyThemeLayer(layer) {
78
- legacyLayer = layer === undefined ? undefined : copyLayer(layer);
79
- defaults = mergeTheme(DEFAULT_THEME, legacyLayer);
80
- legacyVersion++;
85
+ state.defaults = mergeTheme(DEFAULT_THEME, layer === undefined ? undefined : copyLayer(layer));
86
+ state.legacyVersion++;
81
87
  }
82
- // The theme outside any call: the one of the app or testing module that claimed it, until it gives it up
83
- let ambient;
84
- let ambientVersion = 0;
85
88
  /** The number of times the app read outside a call has changed, so each app decides again whether its calls need a scope. */ function ambientThemeVersion() {
86
- return ambientVersion;
87
- }
88
- /** Whether some app's theme is the one read outside a call. */ function hasAmbientTheme() {
89
- return ambient !== undefined;
89
+ return state.ambientVersion;
90
90
  }
91
91
  /**
92
92
  * Makes an app's theme the one read outside a call, when no other app has: a bot runs one app, and code it runs
93
93
  * outside a handler, such as a scheduled job, then reads that app's theme. A second app in the same process keeps
94
94
  * its theme to its own calls.
95
95
  */ function claimAmbientTheme(owner, theme) {
96
- if (ambient && ambient.owner !== owner) return false;
97
- if (!ambient) ambientVersion++;
98
- ambient = {
96
+ if (state.ambient && state.ambient.owner !== owner) return false;
97
+ if (!state.ambient) state.ambientVersion++;
98
+ state.ambient = {
99
99
  owner,
100
100
  theme
101
101
  };
102
102
  return true;
103
103
  }
104
104
  /** Gives up the theme read outside a call, when `owner` has it: its start failed, or it has shut down. */ function releaseAmbientTheme(owner) {
105
- if (ambient?.owner !== owner) return;
106
- ambient = undefined;
107
- ambientVersion++;
105
+ if (state.ambient?.owner !== owner) return;
106
+ state.ambient = undefined;
107
+ state.ambientVersion++;
108
108
  }
109
109
  /** Whether `owner` is the app whose theme is read outside a call. */ function ownsAmbientTheme(owner) {
110
- return ambient?.owner === owner;
110
+ return state.ambient?.owner === owner;
111
111
  }
112
112
  /** Runs `fn` with `theme` as the theme of the call, for everything it runs, awaits or starts. */ function runWithTheme(theme, fn) {
113
113
  return scope.run({
@@ -123,10 +123,8 @@ let ambientVersion = 0;
123
123
  /** Runs `fn` in `themeScope`, whose theme may be replaced while it runs, as a per-server theme is found. */ function runInThemeScope(themeScope, fn) {
124
124
  return scope.run(themeScope, fn);
125
125
  }
126
- // Set by the runtime: the theme an app gives an interaction answered outside any call, as in a collector
127
- let themeOutsideCalls;
128
126
  /** Sets how an answer outside any call finds its theme: from the interaction's app, server and user. */ function setThemeOutsideCalls(resolve) {
129
- themeOutsideCalls = resolve;
127
+ state.themeOutsideCalls = resolve;
130
128
  }
131
129
  /**
132
130
  * The theme an answer to `interaction` uses: the running call's, or, outside any call, as in a collector's callback,
@@ -134,7 +132,7 @@ let themeOutsideCalls;
134
132
  */ function themeForInteraction(interaction) {
135
133
  const call = scope.getStore();
136
134
  if (call) return call.theme;
137
- return themeOutsideCalls?.(interaction) ?? useTheme();
135
+ return state.themeOutsideCalls?.(interaction) ?? useTheme();
138
136
  }
139
137
  /**
140
138
  * The theme of the running call, with every role present.
@@ -163,7 +161,7 @@ let themeOutsideCalls;
163
161
  *
164
162
  * @group Responses
165
163
  */ function useTheme() {
166
- return scope.getStore()?.theme ?? ambient?.theme() ?? defaults;
164
+ return scope.getStore()?.theme ?? state.ambient?.theme() ?? state.defaults;
167
165
  }
168
166
  /**
169
167
  * Makes a function run in the theme of the call that binds it, wherever it is called from later.
@@ -172,6 +170,9 @@ let themeOutsideCalls;
172
170
  * Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
173
171
  * Timers and promises the handler starts keep its theme without it.
174
172
  *
173
+ * The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
174
+ * it is for, is answered in the starting member's theme.
175
+ *
175
176
  * @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
176
177
  * @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
177
178
  *
@@ -181,7 +182,7 @@ let themeOutsideCalls;
181
182
  * async poll(message: Message) {
182
183
  * const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
183
184
  * collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
184
- * const { emojis } = useTheme() // the handler's theme, @UseTheme included
185
+ * const { emojis } = useTheme() // the starting member's theme, @UseTheme included
185
186
  * await click.reply(`${emojis.success} Picked`)
186
187
  * }))
187
188
  * }
@@ -195,4 +196,4 @@ let themeOutsideCalls;
195
196
  };
196
197
  }
197
198
 
198
- export { ambientThemeVersion, bindTheme, claimAmbientTheme, copyLayer, defaultTheme, hasAmbientTheme, mergeTheme, outsideThemeScope, ownsAmbientTheme, releaseAmbientTheme, runInThemeScope, runWithTheme, setLegacyThemeLayer, setThemeOutsideCalls, themeForInteraction, themeLayersVersion, useTheme };
199
+ export { ambientThemeVersion, bindTheme, claimAmbientTheme, copyLayer, defaultTheme, mergeTheme, outsideThemeScope, ownsAmbientTheme, releaseAmbientTheme, runInThemeScope, runWithTheme, setLegacyThemeLayer, setThemeOutsideCalls, themeForInteraction, themeLayersVersion, useTheme };
@@ -51,7 +51,7 @@ const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an arr
51
51
  const prototype = Object.getPrototypeOf(value);
52
52
  return prototype === Object.prototype || prototype === null;
53
53
  };
54
- const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(), since anything else would replace the whole";
54
+ const PLAIN = "give a plain object: a database row's plain form, such as Mongoose's toObject() or Sequelize's get({ plain: true }), or an object literal of the values you set, since anything else would replace the whole";
55
55
  /** What a non-plain object is: by its class, or as one without a plain prototype when it has none named. */ const describeInstance = (value)=>{
56
56
  const name = value.constructor?.name;
57
57
  return typeof name === 'string' && name ? `a ${name}` : 'an object without a plain prototype';
@@ -8,9 +8,12 @@ import { assertObservers } from '../core/observer-runner.js';
8
8
  import { assertValidTheme, themeForProblem } from '../core/theme-validation.js';
9
9
  import { copyLayer } from '../core/theme-scope.js';
10
10
  import { declaring, deprecatedOnMethod, refuse } from '../util/refusal.util.js';
11
- import { timeoutProblem } from '../util/timer-limit.util.js';
11
+ import { timeoutProblem, timerKeepsSeconds, MAX_TIMER_SECONDS } from '../util/timer-limit.util.js';
12
+ import { Logger } from '../common/logger.js';
13
+ import { isConstructor, describeValue } from '../util/value.util.js';
12
14
  import { META } from '../util/metadata-keys.js';
13
15
 
16
+ const logger = new Logger('MeoCord');
14
17
  /** Refuses a `messages` option of the wrong type where the app is declared, rather than at the first message. */ function assertMessageOptions(appName, messages, i18n) {
15
18
  if (!messages) return;
16
19
  const { prefix, mention, caseSensitive } = messages;
@@ -38,6 +41,9 @@ import { META } from '../util/metadata-keys.js';
38
41
  if (deleteUsageRepliesAfter !== undefined && !(typeof deleteUsageRepliesAfter === 'number' && deleteUsageRepliesAfter >= 0 && Number.isFinite(deleteUsageRepliesAfter))) {
39
42
  throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { deleteUsageRepliesAfter } }) takes a number of seconds, or 0 to keep usage replies.`));
40
43
  }
44
+ if (deleteUsageRepliesAfter !== undefined && !timerKeepsSeconds(deleteUsageRepliesAfter)) {
45
+ logger.warn(`${appName}: @MeoCord({ messages: { deleteUsageRepliesAfter } }) is ${deleteUsageRepliesAfter} seconds, longer than a timer ` + `can wait (${MAX_TIMER_SECONDS}), so usage replies are kept. The next major version (5.0) refuses it.`);
46
+ }
41
47
  const { help } = messages;
42
48
  // One word each, since help is asked for by its first word
43
49
  const isWord = (value)=>typeof value === 'string' && /^\S+$/.test(value);
@@ -52,6 +58,35 @@ import { META } from '../util/metadata-keys.js';
52
58
  if (type.labelKey !== undefined) assertLabelKey(appName, name, type.labelKey, i18n);
53
59
  }
54
60
  }
61
+ /**
62
+ * Refuses an option that takes a class given anything else, such as an object or an arrow function, where the app is
63
+ * declared: the startup checks read each class's prototype, and would otherwise fail on it without naming the option.
64
+ */ function assertClassOptions(appName, { presenter, cooldownStore, controllers, services }) {
65
+ for (const [name, value, implementing] of [
66
+ [
67
+ 'presenter',
68
+ presenter,
69
+ 'ResponsePresenter'
70
+ ],
71
+ [
72
+ 'cooldownStore',
73
+ cooldownStore,
74
+ 'CooldownStore'
75
+ ]
76
+ ]){
77
+ if (value !== undefined && !isConstructor(value)) {
78
+ throw refuse(new TypeError(`${appName}: @MeoCord({ ${name} }) takes a class implementing ${implementing}, not ${describeValue(value)}.`));
79
+ }
80
+ }
81
+ for (const controller of controllers ?? []){
82
+ if (!isConstructor(controller)) throw refuse(new TypeError(`${appName}: @MeoCord({ controllers }) takes classes, not ${describeValue(controller)}.`));
83
+ }
84
+ // A string or symbol is a token, which a provider has to provide; that is checked as the app is created
85
+ for (const service of services ?? []){
86
+ if (isConstructor(service) || typeof service === 'string' || typeof service === 'symbol') continue;
87
+ throw refuse(new TypeError(`${appName}: @MeoCord({ services }) takes classes, or tokens that providers provide, not ${describeValue(service)}.`));
88
+ }
89
+ }
55
90
  /** Refuses a type's `labelKey` that no message of the app's default catalog has, as it would be shown as the key. */ function assertLabelKey(appName, name, labelKey, i18n) {
56
91
  const where = `${appName}: @MeoCord({ messages: { types } }): "${name}" has labelKey`;
57
92
  if (typeof labelKey !== 'string') throw refuse(new TypeError(`${where} ${String(labelKey)}; it takes a message key, such as 'types.${name}'.`));
@@ -96,6 +131,7 @@ import { META } from '../util/metadata-keys.js';
96
131
  */ function MeoCord(options) {
97
132
  return declaring((target, propertyKey)=>{
98
133
  if (deprecatedOnMethod('@MeoCord', target, propertyKey)) return;
134
+ assertClassOptions(target.name, options);
99
135
  assertStageEntries('@MeoCord({ guards })', 'guard', target.name, options.guards ?? []);
100
136
  assertStageEntries('@MeoCord({ interceptors })', 'interceptor', target.name, options.interceptors ?? []);
101
137
  assertStageEntries('@MeoCord({ filters })', 'filter', target.name, options.filters ?? []);
@@ -2,7 +2,15 @@ import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
4
  import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
5
+ import { Logger } from '../common/logger.js';
5
6
 
7
+ const logger = new Logger('CommandBuilder');
8
+ /** The nearest base class of `target` that gives `guilds`, which `target` inherits when it gives none. */ function guildsBase(target) {
9
+ for(let base = Object.getPrototypeOf(target); base && base !== Function.prototype; base = Object.getPrototypeOf(base)){
10
+ if (Reflect.hasOwnMetadata(META.builderGuilds, base)) return base;
11
+ }
12
+ return undefined;
13
+ }
6
14
  /**
7
15
  * Marks a class as a command's builder, which describes the command MeoCord registers with Discord.
8
16
  *
@@ -40,6 +48,12 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
40
48
  if (options.guilds) Reflect.defineMetadata(META.builderGuilds, [
41
49
  ...options.guilds
42
50
  ], target);
51
+ else {
52
+ const base = guildsBase(target);
53
+ if (base) {
54
+ logger.warn(`${target.name} gives no guilds and inherits ${base.name}'s; declare guilds on ${target.name} to keep them. ` + 'The next major version (5.0) registers it in the configured scope.');
55
+ }
56
+ }
43
57
  });
44
58
  }
45
59
 
@@ -1,5 +1,5 @@
1
1
  import { dropInheritedRoutes } from './controller.decorator.js';
2
- import { guardOwnHandlersWithBaseGuards } from './guard.decorator.js';
2
+ import { guardOwnHandlersWithBaseGuards } from '../core/guard-runner.js';
3
3
  import { makeInjectable } from '../util/injectable.util.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
5
  import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
@@ -7,8 +7,8 @@ import { wrapsMethod } from '../util/method-wrapper.util.js';
7
7
  import { warnDeprecatedBehaviour } from '../common/deprecation.js';
8
8
  import { Logger } from '../common/logger.js';
9
9
  import { routeSpecificity } from '../core/route-specificity.js';
10
- import { lookupTable, isSegmentType, choicesOf, parseSegment } from '../core/scalar-types.js';
11
- import { declaring, refuse, refuseOnClass } from '../util/refusal.util.js';
10
+ import { lookupTable, isSegmentType, choicesOf, shortestSegment, parseSegment } from '../core/scalar-types.js';
11
+ import { declaring, refuse, refuseOnClass, decoratedName } from '../util/refusal.util.js';
12
12
  import { withArticle, describeValue } from '../util/value.util.js';
13
13
  import { META } from '../util/metadata-keys.js';
14
14
 
@@ -18,6 +18,12 @@ import { META } from '../util/metadata-keys.js';
18
18
  return `; it was given ${describeValue(value)}`;
19
19
  }
20
20
  const logger = new Logger('Command');
21
+ /** The interaction types a method's `@Command` routes declare, each once. */ function routeTypesOf(prototype, method) {
22
+ const metas = Object.values(getCommandMap(prototype) ?? {}).flat();
23
+ return [
24
+ ...new Set(metas.filter((meta)=>meta.methodName === method).map((meta)=>meta.type))
25
+ ];
26
+ }
21
27
  /**
22
28
  * The class's own handler list, started from a copy of the inherited one, so a subclass's
23
29
  * handlers never land in its base class's metadata.
@@ -43,6 +49,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
43
49
  method,
44
50
  label: `autocomplete of ${optionName === undefined ? 'every option' : `"${optionName}"`} in "${commandPath}"`
45
51
  });
52
+ /** An `@On` or `@Once` handler as a warning names its route; one event per method, as a subclass replaces it. */ const eventRoute = ({ method, event })=>({
53
+ method,
54
+ label: `event "${event}"`
55
+ });
46
56
  /** Records a route a class's own decorator declares for a method, which the startup check reads. */ function declareRoute(target, declared) {
47
57
  const routes = Reflect.getOwnMetadata(META.declaredRoutes, target) ?? [];
48
58
  routes.push(declared);
@@ -70,13 +80,22 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
70
80
  else handlers[index] = entry;
71
81
  Reflect.defineMetadata(key, handlers, target);
72
82
  }
83
+ /** The `@On` and `@Once` entries a class's own decorators declare, without those it inherits from its base. */ function ownEventHandlers(prototype) {
84
+ const inherited = Reflect.getMetadata(META.eventHandlers, Object.getPrototypeOf(prototype)) ?? [];
85
+ return ownHandlerList(META.eventHandlers, prototype).filter((entry)=>!inherited.includes(entry));
86
+ }
73
87
  /**
74
88
  * Leaves each method the class's own decorators declare only those routes, of every kind, dropping the ones it inherits
75
89
  * for it: what `@Controller({ inheritedRoutes: 'replace' })` does once the class's method decorators have run.
76
90
  */ function dropInheritedRoutes(prototype) {
77
- const redeclared = new Set(getDeclaredRoutes(prototype).map((route)=>route.method));
78
- if (redeclared.size === 0) return;
79
91
  const base = Object.getPrototypeOf(prototype);
92
+ // The class's own @On and @Once re-declare their methods too
93
+ const ownEvents = ownEventHandlers(prototype);
94
+ const redeclared = new Set([
95
+ ...getDeclaredRoutes(prototype).map((route)=>route.method),
96
+ ...ownEvents.map((entry)=>entry.method)
97
+ ]);
98
+ if (redeclared.size === 0) return;
80
99
  // An entry the class takes from its base, for a method it re-declares
81
100
  const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
82
101
  const commands = ownCommandMap(prototype);
@@ -99,6 +118,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
99
118
  [
100
119
  META.autocompleteHandlers,
101
120
  (entry)=>entry.methodName
121
+ ],
122
+ [
123
+ META.eventHandlers,
124
+ (entry)=>entry.method
102
125
  ]
103
126
  ];
104
127
  for (const [key, method] of lists){
@@ -125,13 +148,14 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
125
148
  function MessageHandler(pattern, options = {}) {
126
149
  return declaring(function(target, propertyKey) {
127
150
  refuseOnClass('@MessageHandler', target, propertyKey);
128
- // An empty pattern means every message, as no pattern does
129
- if (pattern === '') {
130
- warnDeprecatedBehaviour(logger, `@MessageHandler('') on ${target.constructor.name}.${propertyKey}`, 'is refused', '@MessageHandler()');
151
+ // An empty pattern, or one of spaces alone, means every message, as no pattern does
152
+ const blank = pattern !== undefined && pattern.trim() === '';
153
+ if (blank) {
154
+ warnDeprecatedBehaviour(logger, `@MessageHandler('${pattern}') on ${decoratedName(target, propertyKey)}`, 'is refused', '@MessageHandler()');
131
155
  }
132
156
  const method = propertyKey.toString();
133
157
  const declared = {
134
- pattern: pattern || undefined,
158
+ pattern: blank ? undefined : pattern,
135
159
  method,
136
160
  options
137
161
  };
@@ -253,13 +277,14 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
253
277
  params: params.length,
254
278
  typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
255
279
  });
256
- // Each param takes at least one character
280
+ // Each param takes at least the shortest value of its type
281
+ const shortest = literalLength + params.reduce((sum, param)=>sum + shortestSegment(types[param] ?? 'string'), 0);
257
282
  return {
258
283
  regex,
259
284
  params,
260
285
  types: lookupTable(types),
261
286
  specificity,
262
- shortest: literalLength + params.length
287
+ shortest
263
288
  };
264
289
  }
265
290
  /** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
@@ -332,13 +357,17 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
332
357
  refuseOnClass('@Command', target, propertyKey);
333
358
  const originalMethod = _descriptor.value;
334
359
  if (!originalMethod) {
335
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: @Command is on something with no implementation.`));
360
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: @Command is on something with no implementation.`));
336
361
  }
337
362
  // Wrap original method for interaction type validation
338
363
  const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
339
364
  const checked = function(interaction, params) {
340
- if (!matchesCommandType(commandType, interaction)) {
341
- throw new Error(`${target.constructor.name}.${propertyKey}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.`);
365
+ // Every type the method's routes declare, in the class the call is made on: each @Command wraps it, so one alone
366
+ // would refuse the others' interactions, and an override that adds a route type reaches this one through super
367
+ const called = typeof this === 'object' && this !== null && Object.prototype.isPrototypeOf.call(target, this) ? Object.getPrototypeOf(this) : target;
368
+ const types = routeTypesOf(called, propertyKey);
369
+ if (!types.some((type)=>matchesCommandType(type, interaction))) {
370
+ throw new Error(types.length <= 1 ? `${decoratedName(target, propertyKey)}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.` : `${decoratedName(target, propertyKey)}: its @Command routes take ${types.map(interactionClassName).join(' or ')}` + `${givenInstead(interaction)}.`);
342
371
  }
343
372
  return originalMethod.apply(this, [
344
373
  interaction,
@@ -356,7 +385,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
356
385
  let guilds;
357
386
  // Determine command type and builder
358
387
  if (typeof builderOrType === 'function') {
359
- const where = `${target.constructor.name}.${propertyKey}`;
388
+ const where = decoratedName(target, propertyKey);
360
389
  // A subcommand is part of its command's builder, which belongs on the command's own name
361
390
  const subcommandPath = Reflect.getMetadata(META.commandType, builderOrType) === CommandType.SLASH && commandName.includes(' ');
362
391
  const command = commandName.split(' ')[0];
@@ -403,12 +432,12 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
403
432
  if (isCustomIdRouted(commandType)) {
404
433
  // A route warned as it was made
405
434
  const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
406
- if (braces) logger.warn(`${target.constructor.name}.${propertyKey}: in the pattern "${commandName}", ${braces}`);
435
+ if (braces) logger.warn(`${decoratedName(target, propertyKey)}: in the pattern "${commandName}", ${braces}`);
407
436
  let pattern;
408
437
  try {
409
438
  pattern = createRegexFromPattern(commandName);
410
439
  } catch (error) {
411
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: ${error.message}`, {
440
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${error.message}`, {
412
441
  cause: error
413
442
  }));
414
443
  }
@@ -418,7 +447,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
418
447
  // Warned rather than refused, so a bot that carries such a handler still starts
419
448
  if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
420
449
  const length = pattern.shortest === 0 ? 'the pattern is empty' : `its shortest is ${pattern.shortest} characters`;
421
- logger.warn(`${target.constructor.name}.${propertyKey}: the pattern "${commandName}" matches no customId: Discord's customIds ` + `are 1 to ${MAX_CUSTOM_ID_LENGTH} characters, and ${length}, so the handler never runs.`);
450
+ logger.warn(`${decoratedName(target, propertyKey)}: the pattern "${commandName}" matches no customId: Discord's customIds ` + `are 1 to ${MAX_CUSTOM_ID_LENGTH} characters, and ${length}, so the handler never runs.`);
422
451
  }
423
452
  }
424
453
  const declared = {
@@ -470,7 +499,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
470
499
  const registered = built?.type;
471
500
  if (!declared || registered === undefined || declared.kind === registered) return;
472
501
  const kind = registered === ApplicationCommandType.User ? 'user' : 'message';
473
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: it takes a ${declared.name} context menu interaction, but ${builderName} ` + `registers "${commandName}" as a ${kind} context menu command. Declare the handler's interaction as the kind the ` + `builder's setType() names.`));
502
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: it takes a ${declared.name} context menu interaction, but ${builderName} ` + `registers "${commandName}" as a ${kind} context menu command. Declare the handler's interaction as the kind the ` + `builder's setType() names.`));
474
503
  }
475
504
  /**
476
505
  * Retrieves the command map for a given controller.
@@ -608,4 +637,4 @@ const narrowness = (type)=>choicesOf(type) ? 6 : NARROWNESS[type];
608
637
  return values.every((value)=>value !== undefined) ? values.join(PARAM_SEPARATOR) : undefined;
609
638
  }
610
639
 
611
- export { Autocomplete, Command, MAX_CUSTOM_ID_LENGTH, MessageHandler, PARAM_SEPARATOR, ReactionHandler, commandsNamed, createRegexFromPattern, dropInheritedRoutes, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, literalBraces, matchesEmoji, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
640
+ export { Autocomplete, Command, MAX_CUSTOM_ID_LENGTH, MessageHandler, PARAM_SEPARATOR, ReactionHandler, addOwnHandler, commandsNamed, createRegexFromPattern, dropInheritedRoutes, eventRoute, getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers, literalBraces, matchesEmoji, ownEventHandlers, ownHandlerList, paramNarrowness, patternShape, sharedCustomId };
@@ -1,6 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import { nonInteractionHandler, deferMisuseError } from '../core/defer.js';
3
- import { declaring, refuseOnClass, refuse } from '../util/refusal.util.js';
3
+ import { declaring, refuseOnClass, refuse, ownerName } from '../util/refusal.util.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
5
 
6
6
  /**
@@ -43,7 +43,7 @@ import { META } from '../util/metadata-keys.js';
43
43
  refuseOnClass('@Defer', target, propertyKey);
44
44
  const methodName = String(propertyKey);
45
45
  const kind = nonInteractionHandler(target, methodName);
46
- if (kind) throw refuse(deferMisuseError(target.constructor.name, methodName, kind));
46
+ if (kind) throw refuse(deferMisuseError(ownerName(target), methodName, kind));
47
47
  Reflect.defineMetadata(META.deferOptions, {
48
48
  ...options
49
49
  }, target, methodName);
@@ -1,5 +1,5 @@
1
1
  import 'reflect-metadata';
2
- import { ownHandlerList } from './controller.decorator.js';
2
+ import { addOwnHandler } from './controller.decorator.js';
3
3
  import { META } from '../util/metadata-keys.js';
4
4
  import { declaring, refuseOnClass } from '../util/refusal.util.js';
5
5
 
@@ -7,13 +7,12 @@ function eventDecorator(event, once) {
7
7
  // Generic over the method, so it is checked against the event's arguments rather than required to match exactly
8
8
  return declaring(function(target, propertyKey, _descriptor) {
9
9
  refuseOnClass(once ? '@Once' : '@On', target, propertyKey);
10
- const handlers = ownHandlerList(META.eventHandlers, target);
11
- handlers.push({
10
+ // One the class inherits for the same method and event is replaced, as for every other kind of handler
11
+ addOwnHandler(META.eventHandlers, target, {
12
12
  event,
13
13
  method: propertyKey,
14
14
  once
15
- });
16
- Reflect.defineMetadata(META.eventHandlers, handlers, target);
15
+ }, (other)=>other.method === propertyKey && other.event === event);
17
16
  });
18
17
  }
19
18
  /**
@@ -3,9 +3,9 @@ import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { assertStageEntries } from '../core/stage-scope.js';
4
4
  import { Logger } from '../common/logger.js';
5
5
  import { warnDeprecatedBehaviour } from '../common/deprecation.js';
6
- import { isConstructor, describeValue } from '../util/value.util.js';
6
+ import { isConstructible, describeValue } from '../util/value.util.js';
7
7
  import { META } from '../util/metadata-keys.js';
8
- import { declaring, refuseOnMethod } from '../util/refusal.util.js';
8
+ import { declaring, refuseOnMethod, decoratedName } from '../util/refusal.util.js';
9
9
 
10
10
  /**
11
11
  * Marks a class as an exception filter for the given error types.
@@ -40,7 +40,7 @@ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
40
40
  refuseOnMethod('@Catch', target, propertyKey);
41
41
  makeInjectable(target);
42
42
  errorTypes.forEach((type, index)=>{
43
- if (isConstructor(type)) return;
43
+ if (isConstructible(type)) return;
44
44
  const entry = ORDINALS[index] ? `${ORDINALS[index]} entry` : `entry ${index + 1}`;
45
45
  warnDeprecatedBehaviour(logger, `${target.name}: @Catch's ${entry}, ${describeValue(type)}, which matches no error,`, 'is refused', 'an error class, such as @Catch(CooldownError),');
46
46
  });
@@ -88,7 +88,7 @@ const ORDINALS = [
88
88
  * @see {@link https://meocord.dev/docs/4.2/exception-filters | Exception filters}
89
89
  */ function UseFilter(...filters) {
90
90
  return declaring(function(target, propertyKey) {
91
- const where = propertyKey === undefined ? target.name : `${target.constructor.name}.${String(propertyKey)}`;
91
+ const where = decoratedName(target, propertyKey);
92
92
  assertStageEntries('@UseFilter', 'filter', where, filters);
93
93
  // Decorators apply bottom-up, so a higher decorator's filters are tried first.
94
94
  if (propertyKey === undefined) {
@@ -1,47 +1,10 @@
1
1
  import 'reflect-metadata';
2
- import { handlerMethods, classLevelGuards, consumeDispatchMark, runDirectCall, declaringPrototype, classChain } from '../core/guard-runner.js';
2
+ import { applyGuards, recordGuards, handlerMethods, classLevelGuards, declaringPrototype } from '../core/guard-runner.js';
3
3
  import { makeInjectable } from '../util/injectable.util.js';
4
- import { wrapsMethod } from '../util/method-wrapper.util.js';
5
4
  import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
6
5
  import { META } from '../util/metadata-keys.js';
7
- import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
6
+ import { declaring, deprecatedOnMethod, decoratedName } from '../util/refusal.util.js';
8
7
 
9
- /**
10
- * Adds guards to a method's class or method list, then republishes the effective list under
11
- * `META.guards`, in the order they run: the bases' before the class's own, and within one class or method the
12
- * latest decorator first, since it wraps outermost.
13
- */ function recordGuards(key, guards, prototype, methodName) {
14
- const existing = Reflect.getOwnMetadata(key, prototype, methodName) ?? [];
15
- const bases = key === META.classGuards ? Reflect.getOwnMetadata(META.baseClassGuards, prototype, methodName) ?? 0 : 0;
16
- Reflect.defineMetadata(key, [
17
- ...existing.slice(0, bases),
18
- ...guards,
19
- ...existing.slice(bases)
20
- ], prototype, methodName);
21
- const [classGuards, inheritedGuards, methodGuards] = [
22
- META.classGuards,
23
- META.inheritedGuards,
24
- META.methodGuards
25
- ].map((list)=>Reflect.getOwnMetadata(list, prototype, methodName) ?? []);
26
- Reflect.defineMetadata(META.guards, [
27
- ...inheritedGuards,
28
- ...classGuards,
29
- ...methodGuards
30
- ], prototype, methodName);
31
- }
32
- /**
33
- * Wraps a method so a direct call runs its guards first: the wrapper entered first runs the whole
34
- * chain dispatch resolves for the handler, and lets the ones inside it through. A call from dispatch,
35
- * which has already run the guards, passes through every wrapper.
36
- */ function applyGuards(descriptor, prototype, propertyKey) {
37
- const originalMethod = descriptor.value;
38
- descriptor.value = wrapsMethod(async function(...args) {
39
- if (consumeDispatchMark(args[0], this, propertyKey)) return originalMethod.apply(this, args);
40
- return runDirectCall(this, propertyKey, args, ()=>originalMethod.apply(this, args));
41
- }, originalMethod);
42
- const wrappers = Reflect.getOwnMetadata(META.guardWrappers, prototype, propertyKey) ?? 0;
43
- Reflect.defineMetadata(META.guardWrappers, wrappers + 1, prototype, propertyKey);
44
- }
45
8
  /**
46
9
  * The class's own descriptor for a handler. An inherited handler gets one that calls the inherited
47
10
  * method, starting from its guard lists and wrapper count, so class guards wrap it like an own method.
@@ -69,26 +32,6 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
69
32
  ...inherited
70
33
  };
71
34
  }
72
- /**
73
- * Gives each handler a controller declares itself the class-level guards of the classes it extends,
74
- * as its inherited handlers have: recorded under `META.guards`, and wrapped so a direct call
75
- * runs them when no other guard wraps the handler. `@Controller` calls this.
76
- */ function guardOwnHandlersWithBaseGuards(target) {
77
- const prototype = target.prototype;
78
- for (const methodName of handlerMethods(prototype)){
79
- const descriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
80
- // Inherited handlers have the chain of the class that declares them
81
- if (typeof descriptor?.value !== 'function' || Reflect.getOwnMetadata(META.inheritedFrom, prototype, methodName)) continue;
82
- // Base first, as dispatch runs them
83
- const guards = classChain(prototype, prototype).slice(1).reverse().flatMap(classLevelGuards);
84
- if (guards.length === 0) continue;
85
- if (!Reflect.getOwnMetadata(META.guardWrappers, prototype, methodName)) {
86
- applyGuards(descriptor, prototype, methodName);
87
- Object.defineProperty(prototype, methodName, descriptor);
88
- }
89
- recordGuards(META.inheritedGuards, guards, prototype, methodName);
90
- }
91
- }
92
35
  /**
93
36
  * Marks a class as a guard, which decides whether a handler runs.
94
37
  *
@@ -162,7 +105,7 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
162
105
  */ function UseGuard(...entries) {
163
106
  const guards = entries;
164
107
  return declaring(function(target, propertyKey, descriptor) {
165
- const where = propertyKey === undefined ? String(target?.name) : `${target.constructor.name}.${String(propertyKey)}`;
108
+ const where = decoratedName(target, propertyKey);
166
109
  assertStageEntries('@UseGuard', 'guard', where, guards);
167
110
  if (descriptor && propertyKey) {
168
111
  // Method Decorator
@@ -189,4 +132,4 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
189
132
  });
190
133
  }
191
134
 
192
- export { Guard, UseGuard, guardOwnHandlersWithBaseGuards };
135
+ export { Guard, UseGuard };
@@ -1,5 +1,5 @@
1
1
  import { inject } from 'inversify';
2
- import { declaring, refuseOnClass } from '../util/refusal.util.js';
2
+ import { declaring, refuseOnClass, refuse, decoratedName } from '../util/refusal.util.js';
3
3
 
4
4
  /**
5
5
  * Injects what a token provides into a constructor parameter.
@@ -30,6 +30,9 @@ import { declaring, refuseOnClass } from '../util/refusal.util.js';
30
30
  return declaring((target, propertyKey, parameterIndex)=>{
31
31
  // A constructor parameter has no property key either, but has an index
32
32
  if (typeof parameterIndex !== 'number') refuseOnClass('@Inject', target, propertyKey, 'a constructor parameter or a property');
33
+ else if (propertyKey !== undefined) {
34
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: @Inject goes on a constructor parameter or a property, not a method parameter.`));
35
+ }
33
36
  decorate(target, propertyKey, parameterIndex);
34
37
  });
35
38
  }
@@ -2,7 +2,7 @@ import 'reflect-metadata';
2
2
  import { makeInjectable } from '../util/injectable.util.js';
3
3
  import { defineStageTypes, assertStageEntries } from '../core/stage-scope.js';
4
4
  import { META } from '../util/metadata-keys.js';
5
- import { declaring, refuseOnMethod } from '../util/refusal.util.js';
5
+ import { declaring, refuseOnMethod, decoratedName } from '../util/refusal.util.js';
6
6
 
7
7
  /**
8
8
  * Marks a class as an interceptor, which wraps a handler to act before and after it.
@@ -76,7 +76,7 @@ import { declaring, refuseOnMethod } from '../util/refusal.util.js';
76
76
  * @see {@link https://meocord.dev/docs/4.2/interceptors | Interceptors}
77
77
  */ function UseInterceptor(...interceptors) {
78
78
  return declaring(function(target, propertyKey) {
79
- const where = propertyKey === undefined ? target.name : `${target.constructor.name}.${String(propertyKey)}`;
79
+ const where = decoratedName(target, propertyKey);
80
80
  assertStageEntries('@UseInterceptor', 'interceptor', where, interceptors);
81
81
  // Decorators apply bottom-up, so a higher decorator's interceptors go first, as with @UseGuard.
82
82
  if (propertyKey === undefined) {