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.
- package/CHANGELOG.md +188 -0
- package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
- package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
- package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
- package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +435 -182
- package/dist/cjs/decorator/index.cjs +116 -121
- package/dist/cjs/testing/index.cjs +1923 -808
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/package.json.template +9 -9
- package/dist/esm/bin/app-template/src/main.ts.template +3 -2
- package/dist/esm/bin/generator.js +6 -4
- package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
- package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
- package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
- package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
- package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
- package/dist/esm/bin/helper/service-generator.helper.js +6 -4
- package/dist/esm/bin/meocord.js +106 -48
- package/dist/esm/build/native-addons.js +75 -19
- package/dist/esm/build/rsbuild-config.js +11 -3
- package/dist/esm/build/typed-externals.js +15 -2
- package/dist/esm/common/cooldown-store.js +38 -14
- package/dist/esm/common/deprecation.js +4 -1
- package/dist/esm/common/execution-context.js +3 -1
- package/dist/esm/common/explained-error.js +7 -3
- package/dist/esm/common/log-level.js +13 -9
- package/dist/esm/common/logger.js +4 -2
- package/dist/esm/common/meocord-text.js +11 -6
- package/dist/esm/common/redis-cooldown-store.js +3 -7
- package/dist/esm/common/response/flags.js +17 -1
- package/dist/esm/common/response/install-context.js +27 -16
- package/dist/esm/common/response/presenter.js +7 -3
- package/dist/esm/common/response/response-state.js +32 -10
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +27 -8
- package/dist/esm/core/autocomplete-routes.js +26 -0
- package/dist/esm/core/command-conflicts.js +15 -7
- package/dist/esm/core/command-registration.js +15 -1
- package/dist/esm/core/cooldown-runner.js +7 -9
- package/dist/esm/core/defer.js +2 -5
- package/dist/esm/core/dispatcher.js +92 -66
- package/dist/esm/core/event-requirements.js +67 -19
- package/dist/esm/core/event-sources.js +100 -0
- package/dist/esm/core/fallback.js +3 -1
- package/dist/esm/core/filter-runner.js +6 -6
- package/dist/esm/core/guard-runner.js +173 -52
- package/dist/esm/core/handler-input.js +24 -3
- package/dist/esm/core/handler-pipeline.js +54 -32
- package/dist/esm/core/handler-registry.js +51 -21
- package/dist/esm/core/input-runner.js +7 -8
- package/dist/esm/core/interceptor-runner.js +4 -5
- package/dist/esm/core/lifecycle-hooks.js +3 -8
- package/dist/esm/core/lifecycle-order.js +1 -15
- package/dist/esm/core/meocord-classes.js +14 -1
- package/dist/esm/core/meocord-factory.js +83 -54
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +37 -15
- package/dist/esm/core/message-params.js +126 -46
- package/dist/esm/core/message-routes.js +225 -45
- package/dist/esm/core/message-words.js +30 -2
- package/dist/esm/core/observer-runner.js +4 -2
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +39 -4
- package/dist/esm/core/scalar-types.js +57 -16
- package/dist/esm/core/shard-context.js +11 -10
- package/dist/esm/core/shard-exit.js +3 -3
- package/dist/esm/core/shard-manager.js +16 -6
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +28 -8
- package/dist/esm/core/theme-resolvers.js +26 -11
- package/dist/esm/core/theme-runtime.js +23 -13
- package/dist/esm/core/theme-scope.js +31 -30
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +37 -1
- package/dist/esm/decorator/command-builder.decorator.js +14 -0
- package/dist/esm/decorator/controller-class.decorator.js +1 -1
- package/dist/esm/decorator/controller.decorator.js +48 -19
- package/dist/esm/decorator/defer.decorator.js +2 -2
- package/dist/esm/decorator/event.decorator.js +4 -5
- package/dist/esm/decorator/filter.decorator.js +4 -4
- package/dist/esm/decorator/guard.decorator.js +4 -61
- package/dist/esm/decorator/inject.decorator.js +4 -1
- package/dist/esm/decorator/interceptor.decorator.js +2 -2
- package/dist/esm/decorator/theme.decorator.js +4 -3
- package/dist/esm/decorator/validation.decorator.js +5 -5
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +32 -25
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -335
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +217 -91
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +864 -124
- package/dist/esm/testing/response-log.js +8 -0
- package/dist/esm/testing/response.js +7 -5
- package/dist/esm/testing/routing.js +48 -6
- package/dist/esm/testing/strict-mocks.js +8 -4
- package/dist/esm/testing/theme.js +3 -2
- package/dist/esm/util/common.util.js +30 -9
- package/dist/esm/util/generator-cli.util.js +6 -4
- package/dist/esm/util/inherited-env.util.js +72 -13
- package/dist/esm/util/json.util.js +10 -3
- package/dist/esm/util/meocord-cli.util.js +3 -2
- package/dist/esm/util/meocord-config-loader.util.js +14 -1
- package/dist/esm/util/meocord-config-validation.util.js +13 -3
- package/dist/esm/util/meocord-source-config.util.js +13 -3
- package/dist/esm/util/platform.util.js +3 -2
- package/dist/esm/util/refusal.util.js +29 -11
- package/dist/esm/util/registration-mode.util.js +14 -1
- package/dist/esm/util/timer-limit.util.js +4 -1
- package/dist/esm/util/tsconfig-paths.util.js +38 -7
- package/dist/esm/util/tsconfig.util.js +16 -58
- package/dist/esm/util/user-text.util.js +8 -4
- package/dist/esm/util/value.util.js +33 -5
- package/dist/types/common/index.d.cts +17 -10
- package/dist/types/common/index.d.ts +17 -10
- package/dist/types/core/index.d.cts +21 -11
- package/dist/types/core/index.d.ts +21 -11
- package/dist/types/decorator/index.d.cts +63 -63
- package/dist/types/decorator/index.d.ts +63 -63
- package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
- package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
- package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
- package/dist/types/testing/index.d.cts +158 -61
- package/dist/types/testing/index.d.ts +158 -61
- package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
- package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
- package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
- package/package.json +5 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { hasCustomId, describeInteraction,
|
|
1
|
+
import { getReactionHandlers, getMessageHandlers, matchesEmoji } from '../decorator/controller.decorator.js';
|
|
2
|
+
import { hasCustomId, describeInteraction, resolveOptionParams, matchesCommandType } from '../util/interaction.util.js';
|
|
3
|
+
import { buildAutocompleteRoutes, matchAutocompleteRoute } from './autocomplete-routes.js';
|
|
3
4
|
import { buildComponentRoutes, matchComponentRoute } from './component-routes.js';
|
|
4
5
|
import { handleUnroutedError, observeUnclaimed, runHandler, runInAppTheme, globalStagesOf, appPresenterOf } from './handler-pipeline.js';
|
|
5
6
|
import { helpWords, isListable, matchHelpRequest, computeMessageHelp, helpInvocation, renderMessageHelp, splitReply } from './message-help.js';
|
|
@@ -12,7 +13,7 @@ import { handlerInput } from './handler-input.js';
|
|
|
12
13
|
import { matchCommandRoute } from './command-routes.js';
|
|
13
14
|
import { runGuards } from './guard-runner.js';
|
|
14
15
|
import { HandlerRegistry, shareMessageRoutes } from './handler-registry.js';
|
|
15
|
-
import { matchMessageRoute, matchMessageCommand, messageStartsFor, usesAppPrefix, matchMessageSubcommands, commandWordsOf, messageStarts, buildMessageRoutes } from './message-routes.js';
|
|
16
|
+
import { matchMessageRoute, matchMessageCommand, hasMessageText, messageStartsFor, usesAppPrefix, matchMessageSubcommands, commandWordsOf, messageStarts, buildMessageRoutes, messageRouteWarnings } from './message-routes.js';
|
|
16
17
|
import { subcommandUsageError, messageCommandHooks } from './message-params.js';
|
|
17
18
|
import { CommandNotFoundError } from '../common/errors.js';
|
|
18
19
|
import { existingResponse } from '../common/response/response-state.js';
|
|
@@ -83,13 +84,19 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
83
84
|
this.logger.warn(`messages.help is on, but ${route.controllerClass.name}.${route.method} (${JSON.stringify(route.pattern)}) takes "${shown}", ` + `which runs it instead, so the built-in help never answers. Turn help off, or give the handler another word.`);
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* The call for one event, stamped as it is made, so every handler it runs reports one start and a duration that
|
|
89
|
+
* includes routing and any handler that ran before it; the fallback is told of each error that reaches it when there
|
|
90
|
+
* is a recorder.
|
|
91
|
+
*/ callFor(record) {
|
|
87
92
|
const fallback = record ? async (error, context)=>{
|
|
88
93
|
record.unhandled(error);
|
|
89
|
-
|
|
94
|
+
const failure = await this.fallback(error, context);
|
|
95
|
+
if (failure !== undefined) record.unanswered(error, failure);
|
|
96
|
+
return failure;
|
|
90
97
|
} : this.fallback;
|
|
91
98
|
return {
|
|
92
|
-
startedAt,
|
|
99
|
+
startedAt: performance.now(),
|
|
93
100
|
fallback,
|
|
94
101
|
record
|
|
95
102
|
};
|
|
@@ -100,32 +107,35 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
100
107
|
}
|
|
101
108
|
return this.controllerInstancesCache.get(controllerClass);
|
|
102
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Runs `run` on the controller's instance, resolved as its handler starts. A controller that cannot be made fails
|
|
112
|
+
* only its own handlers: the error goes to the global filters, then the fallback, and the call goes on without it.
|
|
113
|
+
*/ async withInstance(controllerClass, args, call, run, unresolved = new Set()) {
|
|
114
|
+
// A controller that failed to resolve for this event is reported once, however many of its handlers it reaches
|
|
115
|
+
if (unresolved.has(controllerClass)) return;
|
|
116
|
+
let instance;
|
|
117
|
+
try {
|
|
118
|
+
instance = this.getInstance(controllerClass);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
unresolved.add(controllerClass);
|
|
121
|
+
await handleUnroutedError(this.container, args, error, this.runOptions(call));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
await run(instance);
|
|
125
|
+
}
|
|
103
126
|
/** The component routes, built on first use; the app calls it as it is created. */ getComponentRoutes() {
|
|
104
127
|
return this.componentRoutes ?? (this.componentRoutes = buildComponentRoutes([
|
|
105
128
|
...this.controllerClasses
|
|
106
129
|
]));
|
|
107
130
|
}
|
|
108
131
|
getAutocompleteRoutes() {
|
|
109
|
-
|
|
110
|
-
const routes = [];
|
|
111
|
-
for (const controllerClass of this.controllerClasses){
|
|
112
|
-
for (const meta of getAutocompleteHandlers(this.getInstance(controllerClass))){
|
|
113
|
-
routes.push({
|
|
114
|
-
controllerClass,
|
|
115
|
-
meta
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
routes.sort((a, b)=>Number(Boolean(b.meta.optionName)) - Number(Boolean(a.meta.optionName)));
|
|
120
|
-
this.autocompleteRoutes = routes;
|
|
121
|
-
return routes;
|
|
132
|
+
return this.autocompleteRoutes ?? (this.autocompleteRoutes = buildAutocompleteRoutes(this.controllerClasses));
|
|
122
133
|
}
|
|
123
134
|
/**
|
|
124
135
|
* Dispatches an interaction. A failure outside any handler, such as no route matching or resolving
|
|
125
136
|
* the controller, goes to the global filters, then the fallback.
|
|
126
137
|
*/ async interaction(interaction, record) {
|
|
127
|
-
|
|
128
|
-
const call = this.callFor(record, performance.now());
|
|
138
|
+
const call = this.callFor(record);
|
|
129
139
|
try {
|
|
130
140
|
await this.dispatchInteraction(interaction, call);
|
|
131
141
|
} catch (error) {
|
|
@@ -151,14 +161,14 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
151
161
|
if (matched) {
|
|
152
162
|
const { route, params } = matched;
|
|
153
163
|
interaction.dynamicParams = params;
|
|
154
|
-
await this.executeCommand(
|
|
164
|
+
await this.executeCommand(route.controllerClass, route.meta, interaction, call);
|
|
155
165
|
return;
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
// The same match a test's invoke checks a handler against, so the two cannot disagree
|
|
159
169
|
const command = matchCommandRoute(this.controllerClasses, interaction);
|
|
160
170
|
if (command) {
|
|
161
|
-
await this.executeCommand(
|
|
171
|
+
await this.executeCommand(command.controllerClass, command.meta, interaction, call);
|
|
162
172
|
return;
|
|
163
173
|
}
|
|
164
174
|
// A component or modal no route takes may be a collector's: with another listener on the client, it has the
|
|
@@ -181,21 +191,17 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
181
191
|
* Answers an autocomplete interaction from the `@Autocomplete` handler that claims it. An unclaimed
|
|
182
192
|
* option gets an empty list and a warning, rather than a menu stuck loading until Discord times out.
|
|
183
193
|
*/ async handleAutocomplete(interaction, call) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
], call);
|
|
196
|
-
if (!ran) await closeAutocomplete(interaction, this.logger);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
194
|
+
const route = matchAutocompleteRoute(this.getAutocompleteRoutes(), interaction);
|
|
195
|
+
if (route) {
|
|
196
|
+
const { controllerClass, meta } = route;
|
|
197
|
+
this.logger.log('[AUTOCOMPLETE]', `[${meta.commandPath}]`, `[${meta.methodName}]`);
|
|
198
|
+
const params = resolveOptionParams(interaction);
|
|
199
|
+
const ran = await this.invokeHandler(this.getInstance(controllerClass), meta.methodName, [
|
|
200
|
+
interaction,
|
|
201
|
+
params
|
|
202
|
+
], call);
|
|
203
|
+
if (!ran) await closeAutocomplete(interaction, this.logger);
|
|
204
|
+
return;
|
|
199
205
|
}
|
|
200
206
|
this.logger.warn(`No handler matched ${describeInteraction(interaction)}. Declare an @Autocomplete handler for it, ` + `or drop setAutocomplete(true) from the option.`);
|
|
201
207
|
await closeAutocomplete(interaction, this.logger);
|
|
@@ -203,19 +209,22 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
203
209
|
interaction
|
|
204
210
|
], this.runOptions(call));
|
|
205
211
|
}
|
|
206
|
-
/** Warns, in development, that a modal field or select menu choice is hidden by a customId param of the same name. */ warnCollisions(methodName, names) {
|
|
212
|
+
/** Warns, in development, that a modal field or select menu choice is hidden by a customId param of the same name. */ warnCollisions(controllerClass, methodName, names) {
|
|
207
213
|
if (process.env.NODE_ENV !== 'development') return;
|
|
214
|
+
let warned = this.warnedCollisions.get(controllerClass);
|
|
215
|
+
if (!warned) this.warnedCollisions.set(controllerClass, warned = new Set());
|
|
208
216
|
for (const name of names){
|
|
209
217
|
const key = `${methodName}:${name}`;
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
this.logger.warn(`"${name}" is both a customId param and a modal field or select menu choice of ${methodName}; the handler receives the customId
|
|
218
|
+
if (warned.has(key)) continue;
|
|
219
|
+
warned.add(key);
|
|
220
|
+
this.logger.warn(`"${name}" is both a customId param and a modal field or select menu choice of ${controllerClass.name}.${methodName}; the handler ` + `receives the customId param. Rename one to receive both.`);
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
223
|
/**
|
|
216
224
|
* Runs a resolved command, shared by both dispatch paths so a pattern-matched
|
|
217
225
|
* component and a named slash command behave identically once the route is chosen.
|
|
218
|
-
*/ async executeCommand(
|
|
226
|
+
*/ async executeCommand(controllerClass, commandMetadata, interaction, call) {
|
|
227
|
+
const controllerInstance = this.getInstance(controllerClass);
|
|
219
228
|
const { methodName, type } = commandMetadata;
|
|
220
229
|
// No interaction-type check here: both callers pick the route with
|
|
221
230
|
// `matchesCommandType` before getting this far, and `@Command` re-checks the
|
|
@@ -223,7 +232,7 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
223
232
|
this.logger.log('[INTERACTION]', `[${type}]`, `[${methodName}]`);
|
|
224
233
|
const routeParams = interaction.dynamicParams;
|
|
225
234
|
const { params, collisions } = handlerInput(interaction, routeParams);
|
|
226
|
-
this.warnCollisions(methodName, collisions);
|
|
235
|
+
this.warnCollisions(controllerClass, methodName, collisions);
|
|
227
236
|
await this.invokeHandler(controllerInstance, methodName, [
|
|
228
237
|
interaction,
|
|
229
238
|
params
|
|
@@ -241,10 +250,7 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
241
250
|
* says whether the handler ran. Its guard wrappers let this call through, so each guard runs once.
|
|
242
251
|
*/ async invokeHandler(instance, methodName, args, call, hooks = {}) {
|
|
243
252
|
const handler = `${instance.constructor.name}.${methodName}`;
|
|
244
|
-
const onUnanswered = this.options.warnUnanswered ? (
|
|
245
|
-
name: returnedBy.interceptor.name,
|
|
246
|
-
handlerStarted: returnedBy.handlerStarted
|
|
247
|
-
}) : undefined;
|
|
253
|
+
const onUnanswered = this.options.warnUnanswered ? (unanswered)=>this.warnUnansweredOnce(handler, unanswered) : undefined;
|
|
248
254
|
const outcome = await runHandler(this.container, instance, methodName, args, {
|
|
249
255
|
...this.runOptions(call),
|
|
250
256
|
onUnanswered,
|
|
@@ -254,13 +260,17 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
254
260
|
return outcome.ran;
|
|
255
261
|
}
|
|
256
262
|
/**
|
|
257
|
-
* Warns, once per handler, that it left its interaction unanswered or deferred without a follow-up,
|
|
258
|
-
*
|
|
259
|
-
*/ warnUnansweredOnce(handler, phase,
|
|
263
|
+
* Warns, once per handler, that it left its interaction or autocomplete unanswered or deferred without a follow-up,
|
|
264
|
+
* or that an interceptor returned before the handler ran or finished, and the call ended so.
|
|
265
|
+
*/ warnUnansweredOnce(handler, { type, phase, returnedBy }) {
|
|
260
266
|
if (this.warnedUnanswered.has(handler)) return;
|
|
261
267
|
this.warnedUnanswered.add(handler);
|
|
268
|
+
const interceptor = returnedBy && {
|
|
269
|
+
name: returnedBy.interceptor.name,
|
|
270
|
+
handlerStarted: returnedBy.handlerStarted
|
|
271
|
+
};
|
|
262
272
|
const returned = interceptor && `${handler}: its interceptor ${interceptor.name} returned before the handler ${interceptor.handlerStarted ? 'finished' : 'ran'}`;
|
|
263
|
-
const what = interceptor ? phase === 'unanswered' ? `${returned}, without answering the interaction, so the user saw "The application did not respond". Answer it ` + `in ${interceptor.name}, or await next.handle().` : interceptor.handlerStarted ? `${returned}, and the deferred interaction had no follow-up when the call ended, so @Defer's lock was ` + `released while the user still saw it thinking. Follow up in ${interceptor.name} with respond(interaction).send(), ` + 'or await next.handle().' : `${returned}, and the deferred interaction was never followed up, so the user saw it thinking until Discord ` + `gave up. Follow up in ${interceptor.name} with respond(interaction).send(), or await next.handle().` : phase === 'unanswered' ? `${handler} finished without answering its interaction, so the user saw "The application did not respond". ` + 'Answer it with respond(interaction).send(), or acknowledge it first with @Defer().' : `${handler} deferred its interaction and never followed up, so the user saw it thinking until Discord gave up. ` + 'Follow up with respond(interaction).send().';
|
|
273
|
+
const what = type === 'autocomplete' ? `${handler} finished without answering its autocomplete, so the user saw its options fail to load. Answer it ` + 'with interaction.respond(choices) within 3 seconds.' : interceptor ? phase === 'unanswered' ? `${returned}, without answering the interaction, so the user saw "The application did not respond". Answer it ` + `in ${interceptor.name}, or await next.handle().` : interceptor.handlerStarted ? `${returned}, and the deferred interaction had no follow-up when the call ended, so @Defer's lock was ` + `released while the user still saw it thinking. Follow up in ${interceptor.name} with respond(interaction).send(), ` + 'or await next.handle().' : `${returned}, and the deferred interaction was never followed up, so the user saw it thinking until Discord ` + `gave up. Follow up in ${interceptor.name} with respond(interaction).send(), or await next.handle().` : phase === 'unanswered' ? `${handler} finished without answering its interaction, so the user saw "The application did not respond". ` + 'Answer it with respond(interaction).send(), or acknowledge it first with @Defer().' : `${handler} deferred its interaction and never followed up, so the user saw it thinking until Discord gave up. ` + 'Follow up with respond(interaction).send().';
|
|
264
274
|
this.logger.warn(`${what} Shown once per handler; @MeoCord({ warnUnanswered: false }) turns it off.`);
|
|
265
275
|
}
|
|
266
276
|
/**
|
|
@@ -269,7 +279,7 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
269
279
|
* usage through that handler's filters. A bare parent's unguarded subcommands' usage, once the app's guards allow
|
|
270
280
|
* it, and a failure to read the prefixes, go through the global filters, then the fallback; the listeners still run.
|
|
271
281
|
*/ async message(message, record) {
|
|
272
|
-
if (message.author.bot || !message.content
|
|
282
|
+
if (message.author.bot || !hasMessageText(message.content)) return;
|
|
273
283
|
const call = this.callFor(record);
|
|
274
284
|
let target;
|
|
275
285
|
try {
|
|
@@ -296,24 +306,29 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
296
306
|
}
|
|
297
307
|
// The matched handler, then every listener, each resolved from the container only as it starts
|
|
298
308
|
const runs = [];
|
|
309
|
+
const unresolved = new Set();
|
|
299
310
|
if (target) {
|
|
300
311
|
const { route, params, start, given } = target;
|
|
301
312
|
noteInvocation(message, `${start}${commandWordsOf(route.tokens).join(' ')}`);
|
|
302
313
|
const hooks = messageCommandHooks(route, params, message, start, given, this.messageOptions.types);
|
|
303
314
|
runs.push({
|
|
304
315
|
name: `${route.controllerClass.name}.${route.method}`,
|
|
305
|
-
run: (at)=>this.
|
|
306
|
-
message
|
|
307
|
-
|
|
308
|
-
|
|
316
|
+
run: (at)=>this.withInstance(route.controllerClass, [
|
|
317
|
+
message
|
|
318
|
+
], at, (instance)=>this.invokeHandler(instance, route.method, [
|
|
319
|
+
message,
|
|
320
|
+
params
|
|
321
|
+
], at, hooks), unresolved)
|
|
309
322
|
});
|
|
310
323
|
}
|
|
311
324
|
for (const { controllerClass, method } of this.messageListeners){
|
|
312
325
|
runs.push({
|
|
313
326
|
name: `${controllerClass.name}.${method}`,
|
|
314
|
-
run: (at)=>this.
|
|
327
|
+
run: (at)=>this.withInstance(controllerClass, [
|
|
315
328
|
message
|
|
316
|
-
], at)
|
|
329
|
+
], at, (instance)=>this.invokeHandler(instance, method, [
|
|
330
|
+
message
|
|
331
|
+
], at), unresolved)
|
|
317
332
|
});
|
|
318
333
|
}
|
|
319
334
|
if (this.messageOptions.handlers === 'concurrent') await this.runTogether(runs, call);
|
|
@@ -338,7 +353,8 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
338
353
|
...call,
|
|
339
354
|
record: {
|
|
340
355
|
settled: (...report)=>reports[index].push(report),
|
|
341
|
-
unhandled: (error)=>record.unhandled(error)
|
|
356
|
+
unhandled: (error)=>record.unhandled(error),
|
|
357
|
+
unanswered: (error, failure)=>record.unanswered(error, failure)
|
|
342
358
|
}
|
|
343
359
|
} : call)));
|
|
344
360
|
for (const report of reports.flat())record?.settled(...report);
|
|
@@ -418,7 +434,7 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
418
434
|
const matching = this.controllerClasses.map((controllerClass)=>({
|
|
419
435
|
controllerClass,
|
|
420
436
|
// Handlers for this emoji first, then those for every emoji
|
|
421
|
-
handlers: getReactionHandlers(
|
|
437
|
+
handlers: getReactionHandlers(controllerClass.prototype).filter(forEmoji).sort((a, b)=>Number(!a.emoji) - Number(!b.emoji))
|
|
422
438
|
})).filter(({ handlers })=>handlers.length > 0);
|
|
423
439
|
if (matching.length === 0) return;
|
|
424
440
|
// Only asked when a matching handler leaves bots out, since a partial user costs a fetch
|
|
@@ -442,16 +458,21 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
442
458
|
return;
|
|
443
459
|
}
|
|
444
460
|
for (const { controllerClass, handlers } of runs){
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
461
|
+
await this.withInstance(controllerClass, [
|
|
462
|
+
reaction,
|
|
463
|
+
{
|
|
464
|
+
user,
|
|
465
|
+
action
|
|
466
|
+
}
|
|
467
|
+
], call, async (instance)=>{
|
|
468
|
+
for (const { method } of handlers)await this.invokeHandler(instance, method, [
|
|
448
469
|
reaction,
|
|
449
470
|
{
|
|
450
471
|
user,
|
|
451
472
|
action
|
|
452
473
|
}
|
|
453
474
|
], call);
|
|
454
|
-
}
|
|
475
|
+
});
|
|
455
476
|
}
|
|
456
477
|
}
|
|
457
478
|
/**
|
|
@@ -470,7 +491,10 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
470
491
|
constructor(options){
|
|
471
492
|
this.options = options;
|
|
472
493
|
this.controllerInstancesCache = new Map();
|
|
473
|
-
/**
|
|
494
|
+
/**
|
|
495
|
+
* Method and name pairs already warned about, by controller class, so a collision warns once per handler, not on
|
|
496
|
+
* every submit or selection. Keyed by the class itself, so two controllers of one name stay apart.
|
|
497
|
+
*/ this.warnedCollisions = new WeakMap();
|
|
474
498
|
/** The handlers already warned about, so each is named once however often it runs. */ this.warnedUnanswered = new Set();
|
|
475
499
|
/** The message handlers already warned about as slow, so each is named once. */ this.warnedSlow = new Set();
|
|
476
500
|
this.container = options.container;
|
|
@@ -485,6 +509,8 @@ import { existingResponse } from '../common/response/response-state.js';
|
|
|
485
509
|
// The app's own help command reads this table too, so it lists the commands dispatch reaches
|
|
486
510
|
if (this.container.isBound(HandlerRegistry)) shareMessageRoutes(this.container.get(HandlerRegistry), this.messageRoutes);
|
|
487
511
|
this.warnUnreachableHelp();
|
|
512
|
+
// Here, where the routes are built once, rather than in buildMessageRoutes, which startup runs twice
|
|
513
|
+
for (const warning of messageRouteWarnings(this.messageRoutes, this.messageOptions))this.logger.warn(warning);
|
|
488
514
|
this.messageListeners = this.controllerClasses.flatMap((controllerClass)=>getMessageHandlers(controllerClass.prototype).filter((handler)=>handler.pattern === undefined).map(({ method })=>({
|
|
489
515
|
controllerClass,
|
|
490
516
|
method
|
|
@@ -13,6 +13,22 @@ const REACTION_PARTIALS = [
|
|
|
13
13
|
Partials.Message,
|
|
14
14
|
Partials.Reaction
|
|
15
15
|
];
|
|
16
|
+
const POLLS = [
|
|
17
|
+
GatewayIntentBits.GuildMessagePolls,
|
|
18
|
+
GatewayIntentBits.DirectMessagePolls
|
|
19
|
+
];
|
|
20
|
+
// discord.js drops a vote on a message it has not cached unless it may build the message, its poll and the answer
|
|
21
|
+
const POLL_PARTIALS = [
|
|
22
|
+
Partials.Message,
|
|
23
|
+
Partials.Poll,
|
|
24
|
+
Partials.PollAnswer
|
|
25
|
+
];
|
|
26
|
+
// The partials whose warning names members and polls; the others keep the wording for messages, reactions and users
|
|
27
|
+
const CACHED_ELSEWHERE = new Set([
|
|
28
|
+
Partials.GuildMember,
|
|
29
|
+
Partials.Poll,
|
|
30
|
+
Partials.PollAnswer
|
|
31
|
+
]);
|
|
16
32
|
/** The intents Discord only sends once they are also enabled for the app in the developer portal. */ const PRIVILEGED = new Set([
|
|
17
33
|
GatewayIntentBits.GuildMembers,
|
|
18
34
|
GatewayIntentBits.GuildPresences,
|
|
@@ -50,10 +66,9 @@ const REACTION_PARTIALS = [
|
|
|
50
66
|
]
|
|
51
67
|
}
|
|
52
68
|
])),
|
|
69
|
+
// A member that joins arrives whole; one that leaves or changes reaches its handler only if cached or with this partial
|
|
53
70
|
...Object.fromEntries([
|
|
54
71
|
'guildMemberAdd',
|
|
55
|
-
'guildMemberRemove',
|
|
56
|
-
'guildMemberUpdate',
|
|
57
72
|
'guildMemberAvailable',
|
|
58
73
|
'threadMembersUpdate'
|
|
59
74
|
].map((event)=>[
|
|
@@ -66,6 +81,26 @@ const REACTION_PARTIALS = [
|
|
|
66
81
|
]
|
|
67
82
|
}
|
|
68
83
|
])),
|
|
84
|
+
guildMemberRemove: {
|
|
85
|
+
intents: [
|
|
86
|
+
[
|
|
87
|
+
GatewayIntentBits.GuildMembers
|
|
88
|
+
]
|
|
89
|
+
],
|
|
90
|
+
partials: [
|
|
91
|
+
Partials.GuildMember
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
guildMemberUpdate: {
|
|
95
|
+
intents: [
|
|
96
|
+
[
|
|
97
|
+
GatewayIntentBits.GuildMembers
|
|
98
|
+
]
|
|
99
|
+
],
|
|
100
|
+
partials: [
|
|
101
|
+
Partials.GuildMember
|
|
102
|
+
]
|
|
103
|
+
},
|
|
69
104
|
guildAuditLogEntryCreate: {
|
|
70
105
|
intents: [
|
|
71
106
|
[
|
|
@@ -198,9 +233,7 @@ const REACTION_PARTIALS = [
|
|
|
198
233
|
intents: [
|
|
199
234
|
REACTIONS
|
|
200
235
|
],
|
|
201
|
-
partials:
|
|
202
|
-
Partials.Reaction
|
|
203
|
-
]
|
|
236
|
+
partials: REACTION_PARTIALS
|
|
204
237
|
},
|
|
205
238
|
typingStart: {
|
|
206
239
|
intents: [
|
|
@@ -212,26 +245,20 @@ const REACTION_PARTIALS = [
|
|
|
212
245
|
},
|
|
213
246
|
messagePollVoteAdd: {
|
|
214
247
|
intents: [
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
]
|
|
219
|
-
]
|
|
248
|
+
POLLS
|
|
249
|
+
],
|
|
250
|
+
partials: POLL_PARTIALS
|
|
220
251
|
},
|
|
221
252
|
messagePollVoteRemove: {
|
|
222
253
|
intents: [
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
]
|
|
227
|
-
]
|
|
254
|
+
POLLS
|
|
255
|
+
],
|
|
256
|
+
partials: POLL_PARTIALS
|
|
228
257
|
},
|
|
229
258
|
...Object.fromEntries([
|
|
230
259
|
'guildScheduledEventCreate',
|
|
231
260
|
'guildScheduledEventDelete',
|
|
232
|
-
'guildScheduledEventUpdate'
|
|
233
|
-
'guildScheduledEventUserAdd',
|
|
234
|
-
'guildScheduledEventUserRemove'
|
|
261
|
+
'guildScheduledEventUpdate'
|
|
235
262
|
].map((event)=>[
|
|
236
263
|
event,
|
|
237
264
|
{
|
|
@@ -242,6 +269,27 @@ const REACTION_PARTIALS = [
|
|
|
242
269
|
]
|
|
243
270
|
}
|
|
244
271
|
])),
|
|
272
|
+
// Each names its user by id alone, which discord.js reads from the cache unless this partial is on
|
|
273
|
+
guildScheduledEventUserAdd: {
|
|
274
|
+
intents: [
|
|
275
|
+
[
|
|
276
|
+
GatewayIntentBits.GuildScheduledEvents
|
|
277
|
+
]
|
|
278
|
+
],
|
|
279
|
+
partials: [
|
|
280
|
+
Partials.User
|
|
281
|
+
]
|
|
282
|
+
},
|
|
283
|
+
guildScheduledEventUserRemove: {
|
|
284
|
+
intents: [
|
|
285
|
+
[
|
|
286
|
+
GatewayIntentBits.GuildScheduledEvents
|
|
287
|
+
]
|
|
288
|
+
],
|
|
289
|
+
partials: [
|
|
290
|
+
Partials.User
|
|
291
|
+
]
|
|
292
|
+
},
|
|
245
293
|
autoModerationRuleCreate: {
|
|
246
294
|
intents: [
|
|
247
295
|
[
|
|
@@ -355,7 +403,7 @@ function intentName(bit) {
|
|
|
355
403
|
(labels.some((label)=>label.startsWith("@MessageHandler('")) ? "; messages: { mention: 'only' } needs none." : '.') : ''));
|
|
356
404
|
}
|
|
357
405
|
for (const [partial, labels] of missingPartials){
|
|
358
|
-
warnings.push(`Partials.${Partials[partial]} is not in clientOptions.partials, so ${labels.join(', ')} will miss ` + (partial === Partials.Channel ? 'direct messages: no DM channel is cached after the bot starts, and discord.js drops a message from a ' + 'channel it has not cached.' : 'events about messages, reactions or users the bot has not cached, such as messages sent before it started.'));
|
|
406
|
+
warnings.push(`Partials.${Partials[partial]} is not in clientOptions.partials, so ${labels.join(', ')} will miss ` + (partial === Partials.Channel ? 'direct messages: no DM channel is cached after the bot starts, and discord.js drops a message from a ' + 'channel it has not cached.' : CACHED_ELSEWHERE.has(partial) ? 'events about members or polls the bot has not cached, such as members who joined before it started.' : 'events about messages, reactions or users the bot has not cached, such as messages sent before it started.'));
|
|
359
407
|
}
|
|
360
408
|
return warnings;
|
|
361
409
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { getEventHandlers } from '../decorator/event.decorator.js';
|
|
2
|
+
import { prepareEventHandler } from './handler-pipeline.js';
|
|
3
|
+
import { isAppClassToken } from './lifecycle-order.js';
|
|
4
|
+
import { isClassProvider } from './providers.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One source for each instance whose lifecycle hooks run, the cooldown store aside, in lifecycle order; read once the
|
|
8
|
+
* providers are made, as a value's or a factory's class is its instance's. A class `prepared` lacks has its handlers
|
|
9
|
+
* prepared here, and one a controller's would be refused for is left out, with `warn` naming the refusal.
|
|
10
|
+
*/ function eventSources(container, providers, units, prepared, warn) {
|
|
11
|
+
// The tokens of each class the container makes, bound as itself or given as a provider's `useClass`, the cooldown
|
|
12
|
+
// store's included: no event reaches it, nor its instance through a factory
|
|
13
|
+
const classTokens = new Map();
|
|
14
|
+
for (const { token } of units){
|
|
15
|
+
const provider = providers.get(token);
|
|
16
|
+
if (provider && !isClassProvider(provider) || !provider && !isAppClassToken(token)) continue;
|
|
17
|
+
const cls = provider ? provider.useClass : token;
|
|
18
|
+
classTokens.set(cls, [
|
|
19
|
+
...classTokens.get(cls) ?? [],
|
|
20
|
+
token
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
const names = new Map(prepared.map((cls)=>[
|
|
24
|
+
cls.name,
|
|
25
|
+
cls
|
|
26
|
+
]));
|
|
27
|
+
const handlers = new Map();
|
|
28
|
+
const handlersOf = (cls)=>{
|
|
29
|
+
let found = handlers.get(cls);
|
|
30
|
+
if (!found) {
|
|
31
|
+
found = prepared.includes(cls) ? getEventHandlers(cls.prototype) : prepareClass(container, cls, names, warn);
|
|
32
|
+
handlers.set(cls, found);
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
};
|
|
36
|
+
const sources = [];
|
|
37
|
+
const given = new Set();
|
|
38
|
+
for (const { token, cooldownStore } of units){
|
|
39
|
+
// The cooldown store is the app's infrastructure, never one of its classes, so no event reaches it
|
|
40
|
+
if (cooldownStore) continue;
|
|
41
|
+
const provider = providers.get(token);
|
|
42
|
+
if (!provider || isClassProvider(provider)) {
|
|
43
|
+
const cls = provider ? provider.useClass : token;
|
|
44
|
+
const found = handlersOf(cls);
|
|
45
|
+
if (found.length > 0) sources.push({
|
|
46
|
+
cls,
|
|
47
|
+
handlers: found,
|
|
48
|
+
resolve: ()=>container.get(token)
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// A value, or a factory's, made by now; one instance under several tokens is one source
|
|
53
|
+
const instance = container.get(token);
|
|
54
|
+
if (typeof instance !== 'object' || instance === null || given.has(instance)) continue;
|
|
55
|
+
given.add(instance);
|
|
56
|
+
const cls = instance.constructor;
|
|
57
|
+
if (typeof cls !== 'function') continue;
|
|
58
|
+
const found = handlersOf(cls);
|
|
59
|
+
if (found.length === 0) continue;
|
|
60
|
+
// A factory may return the instance the container made for a class unit, whose own source runs it; only units of
|
|
61
|
+
// the instance's class are resolved, and each resolves on the event through its own source anyway
|
|
62
|
+
const tokens = classTokens.get(cls) ?? [];
|
|
63
|
+
const owned = ()=>tokens.some((token)=>container.get(token) === instance);
|
|
64
|
+
sources.push({
|
|
65
|
+
cls,
|
|
66
|
+
handlers: found,
|
|
67
|
+
resolve: ()=>owned() ? undefined : instance
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return sources;
|
|
71
|
+
}
|
|
72
|
+
/** The handlers of `cls` that pass a controller's checks, warning of each other one and why it does not run. */ function prepareClass(container, cls, names, warn) {
|
|
73
|
+
const declared = getEventHandlers(cls.prototype);
|
|
74
|
+
if (declared.length === 0) return [];
|
|
75
|
+
const other = names.get(cls.name);
|
|
76
|
+
if (!other) names.set(cls.name, cls);
|
|
77
|
+
// @Once tells classes apart by name, so this class's would share another's
|
|
78
|
+
const sharesName = other !== undefined && other !== cls && declared.some((handler)=>handler.once);
|
|
79
|
+
if (sharesName) {
|
|
80
|
+
refused(warn, cls, `${cls.name}: two classes have this name; @Cooldown and @Once tell classes apart by name, so they would share their counts. Rename one of them.`, declared.filter((handler)=>handler.once));
|
|
81
|
+
}
|
|
82
|
+
const errors = new Map();
|
|
83
|
+
return declared.filter((handler)=>{
|
|
84
|
+
if (sharesName && handler.once) return false;
|
|
85
|
+
let found = errors.get(handler.method);
|
|
86
|
+
if (!found) {
|
|
87
|
+
found = prepareEventHandler(container, cls, handler.method);
|
|
88
|
+
errors.set(handler.method, found);
|
|
89
|
+
const onMethod = declared.filter((other)=>other.method === handler.method && !(sharesName && other.once));
|
|
90
|
+
for (const error of found)refused(warn, cls, error.message, onMethod);
|
|
91
|
+
}
|
|
92
|
+
return found.length === 0;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function refused(warn, cls, reason, handlers) {
|
|
96
|
+
const labels = handlers.map(({ event, method, once })=>`@${once ? 'Once' : 'On'}('${event}') in ${cls.name}.${method}`);
|
|
97
|
+
warn(`${reason} Until the next major version (5.0) refuses it at startup, ${labels.join(', ')} does not run.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { eventSources };
|
|
@@ -9,6 +9,7 @@ import { interactionLocale, translatorOfClient, messageLocale, renderText } from
|
|
|
9
9
|
import { logFailedSend } from '../common/response/send-failure.js';
|
|
10
10
|
import { presenterFor, fitText, TEXT_LIMITS, withSendableFiles, DEFAULT_ATTACHMENT_SIZE_LIMIT, attachmentsOf, renderEmbed, withoutFiles, isTooLarge, REFUSED_AS_TOO_LARGE } from '../common/response/presenter.js';
|
|
11
11
|
import { escapeForLog, quoteForLog } from '../util/user-text.util.js';
|
|
12
|
+
import { timerKeepsSeconds } from '../util/timer-limit.util.js';
|
|
12
13
|
|
|
13
14
|
const UNKNOWN_INTERACTION = 10062;
|
|
14
15
|
/** Discord's refusal of a direct message: the member's are closed, or they blocked the bot. */ const CANNOT_MESSAGE_USER = 50007;
|
|
@@ -280,7 +281,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
280
281
|
parse: []
|
|
281
282
|
}
|
|
282
283
|
}));
|
|
283
|
-
|
|
284
|
+
// Longer than a timer can wait keeps it, as 0 does; the timer would fire at once
|
|
285
|
+
if (seconds > 0 && timerKeepsSeconds(seconds)) {
|
|
284
286
|
setTimeout(()=>{
|
|
285
287
|
reply.delete().catch((failure)=>logFailedSend(logger, 'delete a reply to a command', failure));
|
|
286
288
|
}, seconds * 1000).unref?.();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import { perHandler, sourcePrototype, stageClasses } from './guard-runner.js';
|
|
2
|
+
import { perHandler, sourcePrototype, stageClasses, stageOwners } from './guard-runner.js';
|
|
3
3
|
import { bindShared } from './interceptor-runner.js';
|
|
4
4
|
import { startupError } from '../util/refusal.util.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isConstructible } from '../util/value.util.js';
|
|
6
6
|
import { META } from '../util/metadata-keys.js';
|
|
7
7
|
|
|
8
8
|
/** The filter class an entry names. */ function filterClass(entry) {
|
|
@@ -26,13 +26,13 @@ import { META } from '../util/metadata-keys.js';
|
|
|
26
26
|
];
|
|
27
27
|
}
|
|
28
28
|
/** A handler's method and class filters, the classes innermost first, resolved once. */ const ownFilterLevels = perHandler((prototype, methodName)=>{
|
|
29
|
-
|
|
30
|
-
if (!source) return {
|
|
29
|
+
if (!sourcePrototype(prototype, methodName)) return {
|
|
31
30
|
method: [],
|
|
32
31
|
classes: []
|
|
33
32
|
};
|
|
34
33
|
return {
|
|
35
|
-
|
|
34
|
+
// An override's filters before those of the methods it overrides, innermost first as the classes are
|
|
35
|
+
method: stageOwners(prototype, methodName).reverse().flatMap((owner)=>Reflect.getOwnMetadata(META.methodFilters, owner, methodName) ?? []),
|
|
36
36
|
classes: [
|
|
37
37
|
...stageClasses(prototype, methodName)
|
|
38
38
|
].reverse().flatMap((cls)=>Reflect.getOwnMetadata(META.classFilters, cls) ?? [])
|
|
@@ -51,7 +51,7 @@ import { META } from '../util/metadata-keys.js';
|
|
|
51
51
|
// Every filter reaching here was checked for @Catch at startup, so its types are always recorded
|
|
52
52
|
const types = Reflect.getOwnMetadata(META.catchTypes, filterClass(entry));
|
|
53
53
|
// Only classes match: anything else given to @Catch, warned of as it applied, matches no error
|
|
54
|
-
if (types.length === 0 || types.some((type)=>
|
|
54
|
+
if (types.length === 0 || types.some((type)=>isConstructible(type) && error instanceof type)) return entry;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
return undefined;
|