meocord 4.1.2 → 4.2.1
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 +362 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
- package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
- package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
- package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
- package/dist/cjs/common/index.cjs +4 -4
- package/dist/cjs/core/index.cjs +365 -310
- package/dist/cjs/decorator/index.cjs +85 -61
- package/dist/cjs/testing/index.cjs +2236 -496
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/README.md.template +3 -1
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
- package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +17 -4
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/decorator.js +6 -6
- package/dist/esm/common/deprecation.js +2 -2
- package/dist/esm/common/errors.js +27 -2
- package/dist/esm/common/execution-context.js +1 -1
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/metadata.js +1 -1
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +84 -10
- package/dist/esm/common/route.js +21 -5
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translate-error.js +1 -1
- package/dist/esm/common/translator.js +37 -6
- package/dist/esm/core/command-conflicts.js +34 -34
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/component-routes.js +74 -31
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +51 -7
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +27 -14
- package/dist/esm/core/handler-registry.js +1 -1
- package/dist/esm/core/interceptor-runner.js +2 -2
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +241 -179
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-routes.js +4 -3
- package/dist/esm/core/observer-runner.js +4 -3
- package/dist/esm/core/providers.js +45 -23
- package/dist/esm/core/route-specificity.js +4 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/scalar-types.js +15 -3
- package/dist/esm/core/shard-context.js +6 -7
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/startup-roots.js +23 -4
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-runtime.js +2 -2
- package/dist/esm/core/theme-scope.js +3 -1
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +12 -7
- package/dist/esm/decorator/command-builder.decorator.js +4 -4
- package/dist/esm/decorator/controller-class.decorator.js +9 -4
- package/dist/esm/decorator/controller.decorator.js +162 -83
- package/dist/esm/decorator/cooldown.decorator.js +20 -3
- package/dist/esm/decorator/defer.decorator.js +4 -4
- package/dist/esm/decorator/event.decorator.js +5 -5
- package/dist/esm/decorator/filter.decorator.js +7 -7
- package/dist/esm/decorator/guard.decorator.js +11 -10
- package/dist/esm/decorator/inject.decorator.js +4 -4
- package/dist/esm/decorator/interceptor.decorator.js +7 -7
- package/dist/esm/decorator/observer.decorator.js +4 -4
- package/dist/esm/decorator/service.decorator.js +4 -4
- package/dist/esm/decorator/theme.decorator.js +3 -3
- package/dist/esm/decorator/validation.decorator.js +10 -10
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +27 -11
- package/dist/esm/testing/cooldown-store-suite.js +85 -15
- package/dist/esm/testing/discord-defaults.js +249 -21
- package/dist/esm/testing/index.js +4 -2
- package/dist/esm/testing/inspect-handler.js +20 -3
- package/dist/esm/testing/meocord-testing-module.js +352 -205
- package/dist/esm/testing/mock-fn.js +173 -22
- package/dist/esm/testing/mock-interaction.js +1214 -209
- package/dist/esm/testing/response.js +1 -1
- package/dist/esm/testing/routing.js +39 -14
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/startup-errors.js +26 -0
- package/dist/esm/testing/strict-mocks.js +65 -0
- package/dist/esm/testing/theme.js +1 -1
- package/dist/esm/util/bundle-entry.util.js +2 -1
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +3 -3
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +4 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/metadata-keys.js +1 -0
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/refusal.util.js +148 -8
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +12 -1
- package/dist/types/common/index.d.cts +8 -8
- package/dist/types/common/index.d.ts +8 -8
- package/dist/types/core/index.d.cts +10 -7
- package/dist/types/core/index.d.ts +10 -7
- package/dist/types/decorator/index.d.cts +31 -29
- package/dist/types/decorator/index.d.ts +31 -29
- package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
- package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
- package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
- package/dist/types/testing/index.d.cts +406 -70
- package/dist/types/testing/index.d.ts +406 -70
- package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +4 -3
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var discord_js = require('discord.js');
|
|
4
|
-
var controller_decorator = require('./controller.decorator-
|
|
5
|
-
var providers = require('./providers-
|
|
4
|
+
var controller_decorator = require('./controller.decorator-XKMlcUZz.cjs');
|
|
5
|
+
var providers = require('./providers-BZgJyfZS.cjs');
|
|
6
6
|
var enum_index = require('../enum/index.cjs');
|
|
7
|
-
var
|
|
7
|
+
var node_async_hooks = require('node:async_hooks');
|
|
8
|
+
var translateError = require('./translate-error-BUAR1WYG.cjs');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* How a pattern ranks, one character per segment twice over: `L` for literal text or `P` for a param, then how narrow
|
|
12
|
+
* each param is, the narrowest as the lowest digit. Two keys of one length compare as text: literal before param at
|
|
13
|
+
* the first segment where they differ, then the narrower type.
|
|
14
|
+
*/ function rankKey(pattern) {
|
|
15
|
+
const narrowness = pattern.split('/').map(controller_decorator.paramNarrowness);
|
|
16
|
+
const kinds = narrowness.map((rank)=>rank === undefined ? 'L' : 'P').join('');
|
|
17
|
+
return `${kinds}/${narrowness.map((rank)=>String(rank === undefined ? 0 : 9 - rank)).join('')}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Orders two routes as dispatch tries them. Left to right, the first segment one pattern spells out as literal text
|
|
21
|
+
* while the other leaves it to a param ranks the literal one first: `a/{x}` before `{x}/abcd`, `profile/me/{section}`
|
|
22
|
+
* before `profile/{userId}/edit`. Between patterns that leaves tied, the first param where one type is narrower ranks
|
|
23
|
+
* it first: words to choose from, then `bool`, `uuid`, `snowflake`, `int`, `number`, then text. Patterns of different
|
|
24
|
+
* segment counts, which no customId matches both of, are ordered by count only so the order is total; component
|
|
25
|
+
* patterns have no catch-all param. What remains tied keeps its listing order.
|
|
26
|
+
*/ function rank(keys) {
|
|
27
|
+
return (a, b)=>{
|
|
28
|
+
const [left, right] = [
|
|
29
|
+
keys.get(a),
|
|
30
|
+
keys.get(b)
|
|
31
|
+
];
|
|
32
|
+
if (left.length !== right.length) return left.length - right.length;
|
|
33
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Every customId-pattern route of the given controllers, in the order dispatch tries them, read from metadata alone.
|
|
11
38
|
* Throws for two handlers whose patterns match the same customIds of one component type; one handler
|
|
12
39
|
* declared under two spellings of a pattern keeps one route.
|
|
13
40
|
*/ function buildComponentRoutes(controllerClasses) {
|
|
@@ -40,22 +67,11 @@ var translateError = require('./translate-error-BnM8FbSH.cjs');
|
|
|
40
67
|
}
|
|
41
68
|
}
|
|
42
69
|
}
|
|
43
|
-
// A stable sort, so
|
|
44
|
-
return routes.sort((
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* parameter, the tie-break between equally specific patterns in the next major version (5.0).
|
|
49
|
-
*/ function literalFirst(a, b) {
|
|
50
|
-
const [left, right] = [
|
|
51
|
-
a.split('/'),
|
|
52
|
-
b.split('/')
|
|
53
|
-
];
|
|
54
|
-
for(let i = 0; i < Math.min(left.length, right.length); i++){
|
|
55
|
-
const leftLiteral = !left[i].includes('{');
|
|
56
|
-
if (leftLiteral !== !right[i].includes('{')) return leftLiteral;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
70
|
+
// A stable sort, so what the ranking leaves tied keeps the order controllers and handlers are listed in
|
|
71
|
+
return routes.sort(rank(new Map(routes.map((route)=>[
|
|
72
|
+
route,
|
|
73
|
+
rankKey(route.pattern)
|
|
74
|
+
]))));
|
|
59
75
|
}
|
|
60
76
|
/** A component type as an error names it: `button`, `modal submit`, `select menu`. */ const typeLabel = (type)=>type.toLowerCase().replaceAll('_', ' ');
|
|
61
77
|
/**
|
|
@@ -80,6 +96,12 @@ const typesByPattern = new Map();
|
|
|
80
96
|
if (!types) typesByPattern.set(pattern, types = controller_decorator.createRegexFromPattern(pattern).types);
|
|
81
97
|
return types;
|
|
82
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The params `pattern`, compiled as `regex`, captures from a customId, each typed param as its value:
|
|
101
|
+
* `undefined` when the customId does not match, or a segment is not a value of its param's type.
|
|
102
|
+
*/ function readCustomId(pattern, regex, customId) {
|
|
103
|
+
return readCustomIdSegments(pattern, regex, customId)?.values;
|
|
104
|
+
}
|
|
83
105
|
/** {@link readCustomId}'s values, with each param's text as the customId gave it. */ function readCustomIdSegments(pattern, regex, customId) {
|
|
84
106
|
const match = regex.exec(customId);
|
|
85
107
|
if (!match) return undefined;
|
|
@@ -97,18 +119,40 @@ const typesByPattern = new Map();
|
|
|
97
119
|
text
|
|
98
120
|
};
|
|
99
121
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Pattern pairs of one component type that the ranking leaves tied and that can match one customId, each with the
|
|
124
|
+
* pattern that runs, the one ranked first in `routes`, and an id both match. Only patterns of one rank key, read
|
|
125
|
+
* together, can tie, so each group is compared within itself.
|
|
126
|
+
*/ function findComponentRouteConflicts(routes) {
|
|
127
|
+
const groups = new Map();
|
|
128
|
+
for (const route of routes){
|
|
129
|
+
// The literal text and each param's narrowness: patterns that differ in either rank apart or match no id in common
|
|
130
|
+
const key = `${route.meta.type}\0${route.pattern.split('/').map((segment)=>controller_decorator.paramNarrowness(segment) ?? `=${segment}`).join('\0')}`;
|
|
131
|
+
groups.set(key, [
|
|
132
|
+
...groups.get(key) ?? [],
|
|
133
|
+
route
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
const conflicts = [];
|
|
137
|
+
for (const group of groups.values()){
|
|
138
|
+
for(let i = 0; i < group.length; i++){
|
|
139
|
+
for(let j = i + 1; j < group.length; j++){
|
|
140
|
+
const customId = controller_decorator.sharedCustomId(group[i].pattern, group[j].pattern);
|
|
141
|
+
if (customId === undefined) continue;
|
|
142
|
+
const patterns = [
|
|
143
|
+
group[i].pattern,
|
|
144
|
+
group[j].pattern
|
|
145
|
+
];
|
|
146
|
+
conflicts.push({
|
|
147
|
+
type: group[i].meta.type,
|
|
148
|
+
patterns,
|
|
149
|
+
runs: group[i].pattern,
|
|
150
|
+
customId
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return conflicts;
|
|
112
156
|
}
|
|
113
157
|
|
|
114
158
|
const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.methodName}`;
|
|
@@ -131,7 +175,7 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
|
|
|
131
175
|
* Refuses commands of which only one could run or be registered: two handlers of one slash command name or subcommand
|
|
132
176
|
* path or of one context menu name and kind, or two builders of one command; a command's subcommands, and a user and a
|
|
133
177
|
* message context menu of one name, are no clash. The app, `meocord register`, the shard manager and testing call it.
|
|
134
|
-
*
|
|
178
|
+
* Each clash is a startup error, naming both handlers or both builders with what to do instead; see `startupError`.
|
|
135
179
|
*/ function assertDistinctCommands(controllerClasses) {
|
|
136
180
|
const handlers = new Map();
|
|
137
181
|
const builders = new Map();
|
|
@@ -149,7 +193,8 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
|
|
|
149
193
|
const key = `${meta.type}\0${name}`;
|
|
150
194
|
const earlier = (handlers.get(key) ?? []).find((other)=>overlap(other.meta, meta));
|
|
151
195
|
if (earlier) {
|
|
152
|
-
|
|
196
|
+
controller_decorator.startupError(new Error(`${where(earlier)}: it and ${where(here)} both handle the ${describe(meta.type, name, contextMenuKind(meta) ?? contextMenuKind(earlier.meta))}, ` + `so only ${where(earlier)} would ever run. Keep one handler for it, or give the other a name or ` + `subcommand path of its own.`));
|
|
197
|
+
continue;
|
|
153
198
|
}
|
|
154
199
|
handlers.set(key, [
|
|
155
200
|
...handlers.get(key) ?? [],
|
|
@@ -170,7 +215,7 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
|
|
|
170
215
|
name: typeof body.name === 'string' ? body.name : name
|
|
171
216
|
});
|
|
172
217
|
} else if (built.meta.builderClass !== meta.builderClass) {
|
|
173
|
-
|
|
218
|
+
controller_decorator.startupError(new Error(`${where(built)}: its builder ${built.meta.builderClass.name} and ${meta.builderClass.name} on ${where(here)} both ` + `build the ${describe(meta.type, built.name, contextMenuKind(meta))}, and Discord registers one command ` + `per name and type, so only the first would be. Keep one builder, on a single @Command, and declare ` + `the other handlers with CommandType.${meta.type}.`));
|
|
174
219
|
}
|
|
175
220
|
}
|
|
176
221
|
}
|
|
@@ -348,18 +393,21 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
|
|
|
348
393
|
/**
|
|
349
394
|
* Warns about a handler a subclass re-declares on other routes while it still answers the ones it inherits, for
|
|
350
395
|
* `@Command`, `@MessageHandler`, `@ReactionHandler` and `@Autocomplete` alike. The next major version (5.0) drops
|
|
351
|
-
* the inherited routes. Every class a listed controller extends is checked
|
|
396
|
+
* the inherited routes. Every class a listed controller extends is checked, a base that is not listed included, for
|
|
397
|
+
* the methods the controller still inherits through it.
|
|
352
398
|
*/ function warnInheritedRoutes(controllerClasses) {
|
|
353
|
-
|
|
354
|
-
const problems =
|
|
399
|
+
// A set, so a class two listed controllers extend is named once, for the routes either of them still answers
|
|
400
|
+
const problems = new Set();
|
|
355
401
|
for (const controllerClass of controllerClasses){
|
|
402
|
+
// Methods a class below re-declares with @Controller({ inheritedRoutes: 'replace' }): the listed controller inherits
|
|
403
|
+
// nothing of them from above it, so a class above is not named for them
|
|
404
|
+
const cut = new Set();
|
|
356
405
|
for(let prototype = controllerClass.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
|
|
357
|
-
if (checked.has(prototype)) break;
|
|
358
|
-
checked.add(prototype);
|
|
359
406
|
const declared = controller_decorator.getDeclaredRoutes(prototype);
|
|
360
407
|
const base = Object.getPrototypeOf(prototype);
|
|
361
408
|
if (declared.length === 0 || !base) continue;
|
|
362
|
-
const
|
|
409
|
+
if (Reflect.getOwnMetadata(controller_decorator.META.replacesInheritedRoutes, prototype)) for (const route of declared)cut.add(route.method);
|
|
410
|
+
const inherited = controller_decorator.getHandlerRoutes(base).filter((route)=>!cut.has(route.method));
|
|
363
411
|
for (const method of new Set(declared.map((route)=>route.method))){
|
|
364
412
|
const own = [
|
|
365
413
|
...new Set(declared.filter((route)=>route.method === method).map((route)=>route.label))
|
|
@@ -369,17 +417,19 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
|
|
|
369
417
|
].filter((label)=>!own.includes(label));
|
|
370
418
|
if (kept.length === 0) continue;
|
|
371
419
|
const name = prototype.constructor.name;
|
|
372
|
-
problems.
|
|
420
|
+
problems.add(` ${name}.${method} answers ${controller_decorator.listed(kept)}, which it inherits, as well as its own ${controller_decorator.listed(own)}.`);
|
|
373
421
|
}
|
|
374
422
|
}
|
|
375
423
|
}
|
|
376
|
-
if (problems.
|
|
377
|
-
const one = problems.
|
|
378
|
-
logger.warn(`${problems.
|
|
424
|
+
if (problems.size === 0) return;
|
|
425
|
+
const one = problems.size === 1;
|
|
426
|
+
logger.warn(`${problems.size} re-declared ${one ? 'handler still answers routes it inherits' : 'handlers still answer routes they inherit'}:\n` + `${[
|
|
427
|
+
...problems
|
|
428
|
+
].join('\n')}\nIn the next major version (5.0), a handler's own routes replace the ones it inherits. ` + "To keep an inherited route, declare it on the subclass's method as well; to drop it now, give the subclass " + "@Controller({ inheritedRoutes: 'replace' }).");
|
|
379
429
|
}
|
|
380
430
|
/**
|
|
381
|
-
* Warns of customId patterns of one component type that can match one customId, naming
|
|
382
|
-
*
|
|
431
|
+
* Warns of customId patterns of one component type that rank equally and can match one customId, naming an id both
|
|
432
|
+
* match and the handler that runs. An app with such a pair works, so it starts; the next major version (5.0) refuses.
|
|
383
433
|
* @throws Error for two handlers whose patterns match the same customIds, which the app refuses as well.
|
|
384
434
|
*/ function warnOverlappingPatterns(controllerClasses) {
|
|
385
435
|
const routes = buildComponentRoutes(controllerClasses);
|
|
@@ -389,23 +439,16 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
|
|
|
389
439
|
`${route.meta.type}\0${route.pattern}`,
|
|
390
440
|
route
|
|
391
441
|
]));
|
|
392
|
-
const lines = conflicts.map(({ type, patterns: [left, right] })=>{
|
|
393
|
-
const outcome = ambiguityOutcome(routeOf.get(`${type}\0${left}`), routeOf.get(`${type}\0${right}`));
|
|
394
|
-
return ` "${left}" vs "${right}": ${outcome}`;
|
|
395
|
-
});
|
|
396
|
-
logger.warn(`${conflicts.length} pattern pair(s) can match the same customId, so which one runs is decided by ranking rather ` + `than by the ids themselves:\n${lines.join('\n')}`);
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Which of two overlapping routes runs for the ids both match, and why: `runs` ranks first. Between equally
|
|
400
|
-
* specific patterns, the order they are listed in decides until 5.0, which prefers the earlier literal segment.
|
|
401
|
-
*/ function ambiguityOutcome(runs, other) {
|
|
402
442
|
const name = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.methodName}`;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
443
|
+
const lines = conflicts.map(({ type, patterns: [left, right], customId })=>{
|
|
444
|
+
const [runs, other] = [
|
|
445
|
+
routeOf.get(`${type}\0${left}`),
|
|
446
|
+
routeOf.get(`${type}\0${right}`)
|
|
447
|
+
];
|
|
448
|
+
const listed = runs.controllerClass === other.controllerClass ? 'it is declared first' : 'its controller is listed first';
|
|
449
|
+
return ` "${left}" vs "${right}", which both match "${customId}": ${name(runs)} runs, as ${listed}.`;
|
|
450
|
+
});
|
|
451
|
+
logger.warn(`${conflicts.length} pattern pair(s) rank equally and can match the same customId, so the order they are listed in ` + `decides which one runs:\n${lines.join('\n')}\nMake the patterns distinct: in the next major version (5.0), ` + 'MeoCord refuses to start with such a pair.');
|
|
409
452
|
}
|
|
410
453
|
|
|
411
454
|
const UNKNOWN_INTERACTION = 10062;
|
|
@@ -477,7 +520,10 @@ function errorCode(error) {
|
|
|
477
520
|
return send(reply.withoutFiles);
|
|
478
521
|
}
|
|
479
522
|
}
|
|
480
|
-
/** The text of a reply to a message, after the call's `emojis.warning` when `withEmoji
|
|
523
|
+
/** The text of a reply to a message, after the call's `emojis.warning` when `withEmoji`, cut to fit a message's content. */ function replyText(text, withEmoji) {
|
|
524
|
+
const emoji = withEmoji ? `${controller_decorator.useTheme().emojis.warning} ` : '';
|
|
525
|
+
return emoji + controller_decorator.fitText(text, controller_decorator.TEXT_LIMITS.content - emoji.length);
|
|
526
|
+
}
|
|
481
527
|
/**
|
|
482
528
|
* `text`, answering `error` for `message`: plain, after the call's `emojis.warning` when `withEmoji`, unless the
|
|
483
529
|
* presenter's `messageError` draws it as an embed, coloured as an interaction's view is, with that emoji when it has
|
|
@@ -485,28 +531,33 @@ function errorCode(error) {
|
|
|
485
531
|
* the failure comes back with it, for the caller to report once sent.
|
|
486
532
|
*/ async function presentedReply(message, error, text, withEmoji, logger) {
|
|
487
533
|
const presenter = controller_decorator.presenterFor(message.client);
|
|
534
|
+
const translator = controller_decorator.translatorOfClient(message.client);
|
|
535
|
+
const locale = controller_decorator.messageLocale(message) ?? translator?.defaultLocale ?? 'en-US';
|
|
536
|
+
// An empty or whitespace-only message, which Discord refuses, reads as MeoCord's generic error text, so neither the plain reply nor a presenter is left without one
|
|
537
|
+
const shown = text.trim() === '' ? controller_decorator.renderText(translator, locale, {
|
|
538
|
+
key: 'meocord.fallback.error'
|
|
539
|
+
}) : text;
|
|
488
540
|
const plain = {
|
|
489
541
|
body: {
|
|
490
|
-
content: replyText(
|
|
542
|
+
content: replyText(shown, withEmoji)
|
|
491
543
|
}
|
|
492
544
|
};
|
|
493
545
|
const { messageError } = presenter;
|
|
494
546
|
if (!messageError) return plain;
|
|
495
547
|
try {
|
|
496
548
|
const theme = await controller_decorator.themeForInteraction(message);
|
|
497
|
-
const translator = controller_decorator.translatorOfClient(message.client);
|
|
498
|
-
const locale = controller_decorator.messageLocale(message) ?? translator?.defaultLocale ?? 'en-US';
|
|
499
549
|
const tone = controller_decorator.isUserOutcome(error, message) ? 'warning' : 'danger';
|
|
550
|
+
const presented = {
|
|
551
|
+
message: controller_decorator.fitText(shown, controller_decorator.TEXT_LIMITS.embed),
|
|
552
|
+
error,
|
|
553
|
+
tone
|
|
554
|
+
};
|
|
500
555
|
const drawn = await messageError.call(presenter, {
|
|
501
556
|
message,
|
|
502
557
|
locale,
|
|
503
558
|
mode: 'embed',
|
|
504
559
|
theme
|
|
505
|
-
},
|
|
506
|
-
message: text,
|
|
507
|
-
error,
|
|
508
|
-
tone
|
|
509
|
-
});
|
|
560
|
+
}, presented);
|
|
510
561
|
let view = drawn.color === undefined ? {
|
|
511
562
|
...drawn,
|
|
512
563
|
color: theme.colors.primary
|
|
@@ -563,7 +614,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
563
614
|
await sendReply(reply, (body)=>message.reply({
|
|
564
615
|
...body,
|
|
565
616
|
allowedMentions: {
|
|
566
|
-
repliedUser: false
|
|
617
|
+
repliedUser: false,
|
|
618
|
+
parse: []
|
|
567
619
|
}
|
|
568
620
|
}));
|
|
569
621
|
} catch (replyError) {
|
|
@@ -780,6 +832,46 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
780
832
|
}
|
|
781
833
|
}
|
|
782
834
|
|
|
835
|
+
/**
|
|
836
|
+
* The calls an app is running, which shutdown waits for before the `onShutdown` hooks. A call that asks the app to
|
|
837
|
+
* stop is excused, so a handler awaiting `app.stop()` never waits for itself.
|
|
838
|
+
*/ class RunningCalls {
|
|
839
|
+
/** Runs `run` as a call named `label`, tracked until it settles. */ run(label, run) {
|
|
840
|
+
const token = {};
|
|
841
|
+
this.calls.set(token, label);
|
|
842
|
+
const done = this.current.run(token, run);
|
|
843
|
+
const settle = ()=>{
|
|
844
|
+
this.calls.delete(token);
|
|
845
|
+
this.notify();
|
|
846
|
+
};
|
|
847
|
+
done.then(settle, settle);
|
|
848
|
+
return done;
|
|
849
|
+
}
|
|
850
|
+
/** Excuses the call this runs in, and any it runs within, from the wait; outside a call, nothing. */ excuseCurrent() {
|
|
851
|
+
const token = this.current.getStore();
|
|
852
|
+
if (!token || !this.calls.has(token)) return;
|
|
853
|
+
this.excused.add(token);
|
|
854
|
+
this.notify();
|
|
855
|
+
}
|
|
856
|
+
/** The names of the calls a wait would still wait for. */ running() {
|
|
857
|
+
return [
|
|
858
|
+
...this.calls
|
|
859
|
+
].filter(([token])=>!this.excused.has(token)).map(([, label])=>label);
|
|
860
|
+
}
|
|
861
|
+
/** Settles once every call not excused has, those that start or are excused meanwhile included. */ async settled() {
|
|
862
|
+
while(this.running().length > 0)await (this.changed ?? (this.changed = Promise.withResolvers())).promise;
|
|
863
|
+
}
|
|
864
|
+
notify() {
|
|
865
|
+
this.changed?.resolve();
|
|
866
|
+
this.changed = undefined;
|
|
867
|
+
}
|
|
868
|
+
constructor(){
|
|
869
|
+
this.current = new node_async_hooks.AsyncLocalStorage();
|
|
870
|
+
/** Each call under way, by its token, with the name a log gives it. */ this.calls = new Map();
|
|
871
|
+
this.excused = new WeakSet();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
783
875
|
/** The entry of a unit whose instance is `instance`. */ function lifecycleEntry(unit, instance) {
|
|
784
876
|
return {
|
|
785
877
|
name: unit.name,
|
|
@@ -791,11 +883,45 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
791
883
|
}
|
|
792
884
|
};
|
|
793
885
|
}
|
|
886
|
+
/**
|
|
887
|
+
* The ready hooks' pass, which shutdown waits for before it reads which units to shut down, so a unit whose `onReady`
|
|
888
|
+
* finishes meanwhile is shut down too. An `onReady` still running that asks the app to stop is not waited for; a timer
|
|
889
|
+
* or callback an `onReady` that has settled left behind excuses nothing.
|
|
890
|
+
*/ class ReadyPass {
|
|
891
|
+
/** Runs `pass`, the ready hooks, as the pass shutdown waits for. */ run(pass) {
|
|
892
|
+
return this.passes.run('onReady', pass);
|
|
893
|
+
}
|
|
894
|
+
/** Excuses the pass when this runs within the `onReady` in progress, as one that stops the app does. */ excuseCurrent() {
|
|
895
|
+
if (this.current && this.hooks.getStore() === this.current) this.passes.excuseCurrent();
|
|
896
|
+
}
|
|
897
|
+
/** Settles once the pass has, unless excused. */ settled() {
|
|
898
|
+
return this.passes.settled();
|
|
899
|
+
}
|
|
900
|
+
/** The unit whose `onReady` a wait would still wait for, if any. */ running() {
|
|
901
|
+
return this.passes.running().length > 0 ? [
|
|
902
|
+
this.unit ?? 'a unit'
|
|
903
|
+
] : [];
|
|
904
|
+
}
|
|
905
|
+
constructor(){
|
|
906
|
+
this.passes = new RunningCalls();
|
|
907
|
+
/** The `onReady` each async context belongs to. */ this.hooks = new node_async_hooks.AsyncLocalStorage();
|
|
908
|
+
/** Runs a unit's `onReady` as the one in progress. */ this.runOnReady = async (unit, run)=>{
|
|
909
|
+
const hook = {};
|
|
910
|
+
this.current = hook;
|
|
911
|
+
this.unit = unit.name;
|
|
912
|
+
try {
|
|
913
|
+
await this.hooks.run(hook, run);
|
|
914
|
+
} finally{
|
|
915
|
+
if (this.current === hook) this.current = undefined;
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
794
920
|
/**
|
|
795
921
|
* Resolves each unit and runs its `onReady` one at a time, in the units' dependency order. A failure is
|
|
796
922
|
* reported and the next unit still runs. Each unit whose `onReady` settled, or that has none, is pushed
|
|
797
923
|
* to `entries` as it finishes, so a shutdown that starts meanwhile closes exactly those.
|
|
798
|
-
*/ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled } = {}) {
|
|
924
|
+
*/ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled, runOnReady } = {}) {
|
|
799
925
|
const failed = new Set();
|
|
800
926
|
// For each unit, the failed units it depends on, directly or through another dependency
|
|
801
927
|
const failedUpstream = new Map();
|
|
@@ -803,6 +929,9 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
803
929
|
unit.token,
|
|
804
930
|
unit
|
|
805
931
|
]));
|
|
932
|
+
// The token each instance was first reached by: one instance two tokens reach, an alias or one value provided twice,
|
|
933
|
+
// runs its hooks once
|
|
934
|
+
const firstToken = new Map();
|
|
806
935
|
for (const unit of units){
|
|
807
936
|
if (stopped()) break;
|
|
808
937
|
const upstream = new Set();
|
|
@@ -821,9 +950,17 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
821
950
|
settled?.(unit);
|
|
822
951
|
continue;
|
|
823
952
|
}
|
|
953
|
+
const first = firstToken.get(instance);
|
|
954
|
+
if (first !== undefined) {
|
|
955
|
+
// Failed or not as the token that ran its hooks did, for what depends on this one
|
|
956
|
+
if (failed.has(first)) failed.add(unit.token);
|
|
957
|
+
settled?.(unit);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
firstToken.set(instance, unit.token);
|
|
824
961
|
const entry = lifecycleEntry(unit, instance);
|
|
825
|
-
// Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready
|
|
826
|
-
//
|
|
962
|
+
// Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready waits for the one still
|
|
963
|
+
// starting, within its time, and skips those not reached yet
|
|
827
964
|
if (typeof instance.onReady !== 'function') {
|
|
828
965
|
entries.push(entry);
|
|
829
966
|
settled?.(unit);
|
|
@@ -834,7 +971,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
834
971
|
]);
|
|
835
972
|
const slow = slowAfterMs !== undefined && report.slow ? setTimeout(()=>report.slow(unit), slowAfterMs) : undefined;
|
|
836
973
|
try {
|
|
837
|
-
|
|
974
|
+
const onReady = ()=>instance.onReady(client, info);
|
|
975
|
+
await (runOnReady ? runOnReady(unit, onReady) : onReady());
|
|
838
976
|
} catch (error) {
|
|
839
977
|
failed.add(unit.token);
|
|
840
978
|
report.hookFailed(unit, error);
|
|
@@ -847,12 +985,14 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
847
985
|
}
|
|
848
986
|
/**
|
|
849
987
|
* Runs the entries' `onShutdown` hooks one at a time, in reverse, so a unit stops before those it
|
|
850
|
-
* depends on. A failure is reported and the next hook still runs.
|
|
851
|
-
|
|
988
|
+
* depends on. A failure is reported and the next hook still runs. An instance already in `ran` is not shut down again,
|
|
989
|
+
* so one that two entries reach stops once.
|
|
990
|
+
*/ async function runShutdownHooks(entries, hookFailed, ran = new Set()) {
|
|
852
991
|
for (const { name, instance } of [
|
|
853
992
|
...entries
|
|
854
993
|
].reverse()){
|
|
855
|
-
if (typeof instance.onShutdown !== 'function') continue;
|
|
994
|
+
if (typeof instance.onShutdown !== 'function' || ran.has(instance)) continue;
|
|
995
|
+
ran.add(instance);
|
|
856
996
|
try {
|
|
857
997
|
await instance.onShutdown();
|
|
858
998
|
} catch (error) {
|
|
@@ -860,54 +1000,91 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
860
1000
|
}
|
|
861
1001
|
}
|
|
862
1002
|
}
|
|
863
|
-
/**
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
* reverse dependency order, and the store and what it injects last. The whole sequence is waited for at most
|
|
868
|
-
* `timeoutMs`.
|
|
869
|
-
*/ async function runShutdownSequence(container, entries, { drainCalls, timeoutMs, hookFailed, warn }) {
|
|
870
|
-
// A unit whose onReady settles while the calls drain was still starting when shutdown began, so it is not shut down
|
|
871
|
-
entries = [
|
|
872
|
-
...entries
|
|
873
|
-
];
|
|
874
|
-
const byToken = new Map(entries.map((entry)=>[
|
|
875
|
-
entry.token,
|
|
876
|
-
entry
|
|
877
|
-
]));
|
|
878
|
-
// The store and everything it reaches through what it injects
|
|
879
|
-
const storeSide = new Set();
|
|
880
|
-
const reach = (entry)=>{
|
|
881
|
-
if (!entry || storeSide.has(entry)) return;
|
|
882
|
-
storeSide.add(entry);
|
|
883
|
-
for (const dependency of entry.dependencies)reach(byToken.get(dependency));
|
|
884
|
-
};
|
|
885
|
-
for (const entry of entries)if (entry.cooldownStore) reach(entry);
|
|
886
|
-
const needsStore = [
|
|
887
|
-
...storeSide
|
|
888
|
-
].some((entry)=>typeof entry.instance.onShutdown === 'function');
|
|
889
|
-
const sequence = (async ()=>{
|
|
890
|
-
if (needsStore) {
|
|
891
|
-
await drainCalls();
|
|
892
|
-
await providers.storeOperationsSettled(container);
|
|
893
|
-
}
|
|
894
|
-
// Nothing on the store's side injects anything outside it, so running it last keeps the reverse dependency order
|
|
895
|
-
await runShutdownHooks(entries.filter((entry)=>!storeSide.has(entry)), hookFailed);
|
|
896
|
-
await runShutdownHooks(entries.filter((entry)=>storeSide.has(entry)), hookFailed);
|
|
897
|
-
})();
|
|
1003
|
+
/** The part of `timeoutMs` the drain leaves for the hooks: a quarter, at least 1 s, and never more than half. */ function hooksReserveMs(timeoutMs) {
|
|
1004
|
+
return Math.min(Math.max(timeoutMs / 4, 1000), timeoutMs / 2);
|
|
1005
|
+
}
|
|
1006
|
+
/** A timer whose promise resolves with `value` after `ms`, and `cancel` to clear it. */ function after(ms, value) {
|
|
898
1007
|
let timer;
|
|
899
|
-
const
|
|
900
|
-
timer = setTimeout(()=>resolve(
|
|
1008
|
+
const promise = new Promise((resolve)=>{
|
|
1009
|
+
timer = setTimeout(()=>resolve(value), ms);
|
|
901
1010
|
});
|
|
1011
|
+
return {
|
|
1012
|
+
promise,
|
|
1013
|
+
cancel: ()=>clearTimeout(timer)
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Shuts down the entries there are once an `onReady` in progress has settled, as a bot and a testing module both do.
|
|
1018
|
+
* The calls under way finish first, on every shutdown with `drainAlways`, and otherwise when the cooldown store, or
|
|
1019
|
+
* anything it injects directly or through another, has an `onShutdown`; then the store operations they started, so the
|
|
1020
|
+
* store's last writes and releases still reach it. The other hooks then run in reverse dependency order, and the store
|
|
1021
|
+
* and what it injects last.
|
|
1022
|
+
*
|
|
1023
|
+
* The whole sequence is waited for at most `timeoutMs`. The wait for the `onReady` and the drain stop early enough to
|
|
1024
|
+
* leave the hooks {@link hooksReserveMs}, with a warning naming what still runs, and the hooks run in the time left.
|
|
1025
|
+
*/ async function runShutdownSequence(container, readEntries, { drainCalls, drainAlways = false, runningCalls, readyPass, timeoutMs, hookFailed, warn }) {
|
|
1026
|
+
const whole = after(timeoutMs, 'timeout');
|
|
1027
|
+
const reserve = hooksReserveMs(timeoutMs);
|
|
1028
|
+
const drainEnds = after(timeoutMs - reserve, false);
|
|
1029
|
+
// At once on a shutdown that always drains, so no call starts while an onReady finishes
|
|
1030
|
+
let draining = drainAlways ? drainCalls().then(()=>true) : undefined;
|
|
902
1031
|
try {
|
|
1032
|
+
const readying = readyPass?.running() ?? [];
|
|
1033
|
+
if (readying.length > 0 && !await Promise.race([
|
|
1034
|
+
readyPass.settled().then(()=>true),
|
|
1035
|
+
drainEnds.promise
|
|
1036
|
+
])) {
|
|
1037
|
+
warn(`onReady in ${readying.join(', ')} is still running after ${timeoutMs - reserve} ms; shutting down without waiting for it.`);
|
|
1038
|
+
}
|
|
1039
|
+
// Read once the onReady in progress has settled, so its unit is among them
|
|
1040
|
+
const entries = [
|
|
1041
|
+
...readEntries()
|
|
1042
|
+
];
|
|
1043
|
+
const byToken = new Map(entries.map((entry)=>[
|
|
1044
|
+
entry.token,
|
|
1045
|
+
entry
|
|
1046
|
+
]));
|
|
1047
|
+
// The store and everything it reaches through what it injects
|
|
1048
|
+
const storeSide = new Set();
|
|
1049
|
+
const reach = (entry)=>{
|
|
1050
|
+
if (!entry || storeSide.has(entry)) return;
|
|
1051
|
+
storeSide.add(entry);
|
|
1052
|
+
for (const dependency of entry.dependencies)reach(byToken.get(dependency));
|
|
1053
|
+
};
|
|
1054
|
+
for (const entry of entries)if (entry.cooldownStore) reach(entry);
|
|
1055
|
+
const needsStore = [
|
|
1056
|
+
...storeSide
|
|
1057
|
+
].some((entry)=>typeof entry.instance.onShutdown === 'function');
|
|
1058
|
+
let drained = true;
|
|
1059
|
+
if (needsStore) draining ?? (draining = drainCalls().then(()=>true));
|
|
1060
|
+
if (draining) {
|
|
1061
|
+
drained = await Promise.race([
|
|
1062
|
+
draining,
|
|
1063
|
+
drainEnds.promise
|
|
1064
|
+
]);
|
|
1065
|
+
if (!drained) {
|
|
1066
|
+
const running = runningCalls?.() ?? [];
|
|
1067
|
+
warn(`Calls still running after ${timeoutMs - reserve} ms${running.length > 0 ? `: ${running.join(', ')}` : ''}; ` + `running the onShutdown hooks in the ${reserve} ms left.`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
drainEnds.cancel();
|
|
1071
|
+
const sequence = (async ()=>{
|
|
1072
|
+
// Only after the calls settled: a call still running may never let its store operations settle
|
|
1073
|
+
if (needsStore && drained) await providers.storeOperationsSettled(container);
|
|
1074
|
+
// Nothing on the store's side injects anything outside it, so running it last keeps the reverse dependency order
|
|
1075
|
+
const ran = new Set();
|
|
1076
|
+
await runShutdownHooks(entries.filter((entry)=>!storeSide.has(entry)), hookFailed, ran);
|
|
1077
|
+
await runShutdownHooks(entries.filter((entry)=>storeSide.has(entry)), hookFailed, ran);
|
|
1078
|
+
})();
|
|
903
1079
|
if (await Promise.race([
|
|
904
1080
|
sequence,
|
|
905
|
-
|
|
1081
|
+
whole.promise
|
|
906
1082
|
]) === 'timeout') {
|
|
907
1083
|
warn(`onShutdown hooks did not finish within ${timeoutMs} ms; shutting down anyway.`);
|
|
908
1084
|
}
|
|
909
1085
|
} finally{
|
|
910
|
-
|
|
1086
|
+
drainEnds.cancel();
|
|
1087
|
+
whole.cancel();
|
|
911
1088
|
}
|
|
912
1089
|
}
|
|
913
1090
|
/** Settles once every promise in `calls` has, those added while waiting included. */ async function callsSettled(calls) {
|
|
@@ -930,7 +1107,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
930
1107
|
*/ function matchCommandRoute(controllers, interaction) {
|
|
931
1108
|
for (const key of commandRouteKeys(interaction)){
|
|
932
1109
|
for (const controllerClass of controllers){
|
|
933
|
-
const meta = controller_decorator.getCommandMap(controllerClass.prototype)
|
|
1110
|
+
const meta = controller_decorator.commandsNamed(controller_decorator.getCommandMap(controllerClass.prototype), key).find((each)=>controller_decorator.matchesHandler(each, interaction));
|
|
934
1111
|
if (meta) return {
|
|
935
1112
|
controllerClass,
|
|
936
1113
|
meta
|
|
@@ -954,8 +1131,8 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
954
1131
|
for (const [customId, field] of fields){
|
|
955
1132
|
const data = field;
|
|
956
1133
|
// An upload's `values` are the attachments' ids; discord.js resolves the attachments themselves alongside
|
|
957
|
-
if (data.type === discord_js.ComponentType.FileUpload) values
|
|
958
|
-
else values
|
|
1134
|
+
if (data.type === discord_js.ComponentType.FileUpload) controller_decorator.setOwn(values, customId, entriesOf(interaction.fields.getUploadedFiles(customId)));
|
|
1135
|
+
else controller_decorator.setOwn(values, customId, 'value' in data ? data.value : data.values);
|
|
959
1136
|
}
|
|
960
1137
|
return values;
|
|
961
1138
|
}
|
|
@@ -1014,7 +1191,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1014
1191
|
collisions: []
|
|
1015
1192
|
};
|
|
1016
1193
|
const fields = interaction instanceof discord_js.ModalSubmitInteraction ? modalFields(interaction) : selectChoices(interaction);
|
|
1017
|
-
const collisions = Object.keys(routeParams).filter((name)=>name
|
|
1194
|
+
const collisions = Object.keys(routeParams).filter((name)=>Object.hasOwn(fields, name));
|
|
1018
1195
|
return {
|
|
1019
1196
|
params: {
|
|
1020
1197
|
...fields,
|
|
@@ -1100,6 +1277,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1100
1277
|
if (state && state !== 'unanswered') return true;
|
|
1101
1278
|
return 'replied' in interaction && (interaction.replied || interaction.deferred);
|
|
1102
1279
|
}
|
|
1280
|
+
/** How long a message's handler may take, with others waiting after it, before it is named as slow. */ const SLOW_HANDLER_MS = 5000;
|
|
1103
1281
|
/**
|
|
1104
1282
|
* Routes interactions, messages and reactions to the handlers that take them, as the bot does, and runs
|
|
1105
1283
|
* each through its pipeline. The app and the testing module share it, so a test routes exactly as the bot.
|
|
@@ -1355,22 +1533,64 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1355
1533
|
message
|
|
1356
1534
|
], error, this.runOptions(call));
|
|
1357
1535
|
}
|
|
1536
|
+
// The matched handler, then every listener, each resolved from the container only as it starts
|
|
1537
|
+
const runs = [];
|
|
1358
1538
|
if (target) {
|
|
1359
1539
|
const { route, params, start, given } = target;
|
|
1360
1540
|
noteInvocation(message, `${start}${providers.commandWordsOf(route.tokens).join(' ')}`);
|
|
1361
1541
|
const hooks = providers.messageCommandHooks(route, params, message, start, given, this.messageOptions.types);
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1542
|
+
runs.push({
|
|
1543
|
+
name: `${route.controllerClass.name}.${route.method}`,
|
|
1544
|
+
run: (at)=>this.invokeHandler(this.getInstance(route.controllerClass), route.method, [
|
|
1545
|
+
message,
|
|
1546
|
+
params
|
|
1547
|
+
], at, hooks)
|
|
1548
|
+
});
|
|
1366
1549
|
}
|
|
1367
1550
|
for (const { controllerClass, method } of this.messageListeners){
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1551
|
+
runs.push({
|
|
1552
|
+
name: `${controllerClass.name}.${method}`,
|
|
1553
|
+
run: (at)=>this.invokeHandler(this.getInstance(controllerClass), method, [
|
|
1554
|
+
message
|
|
1555
|
+
], at)
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
if (this.messageOptions.handlers === 'concurrent') await this.runTogether(runs, call);
|
|
1559
|
+
else await this.runInTurn(runs, call);
|
|
1560
|
+
}
|
|
1561
|
+
/** Runs a message's handlers one after another, warning once about one that held the rest back for long. */ async runInTurn(runs, call) {
|
|
1562
|
+
const warn = this.messageOptions.slowHandlerWarning ?? this.options.warnSlowMessageHandlers ?? true;
|
|
1563
|
+
for (const [index, { name, run }] of runs.entries()){
|
|
1564
|
+
const started = performance.now();
|
|
1565
|
+
await run(call);
|
|
1566
|
+
const waiting = runs.length - 1 - index;
|
|
1567
|
+
if (warn && waiting > 0) this.warnSlowOnce(name, performance.now() - started, waiting);
|
|
1371
1568
|
}
|
|
1372
1569
|
}
|
|
1373
1570
|
/**
|
|
1571
|
+
* Starts a message's handlers together and settles once all have. Each is reported as settled in the order the
|
|
1572
|
+
* handlers are listed, whichever settles first, and a call that rejects, rejects this once every one has settled.
|
|
1573
|
+
*/ async runTogether(runs, call) {
|
|
1574
|
+
const { record } = call;
|
|
1575
|
+
const reports = runs.map(()=>[]);
|
|
1576
|
+
const results = await Promise.allSettled(runs.map(({ run }, index)=>run(record ? {
|
|
1577
|
+
...call,
|
|
1578
|
+
record: {
|
|
1579
|
+
settled: (...report)=>reports[index].push(report),
|
|
1580
|
+
unhandled: (error)=>record.unhandled(error)
|
|
1581
|
+
}
|
|
1582
|
+
} : call)));
|
|
1583
|
+
for (const report of reports.flat())record?.settled(...report);
|
|
1584
|
+
const failed = results.find((result)=>result.status === 'rejected');
|
|
1585
|
+
if (failed) throw failed.reason;
|
|
1586
|
+
}
|
|
1587
|
+
/** Warns, once per handler, that it took `SLOW_HANDLER_MS` or longer with `waiting` handlers held back behind it. */ warnSlowOnce(name, took, waiting) {
|
|
1588
|
+
if (took < SLOW_HANDLER_MS || this.warnedSlow.has(name)) return;
|
|
1589
|
+
this.warnedSlow.add(name);
|
|
1590
|
+
const held = waiting === 1 ? 'the listener after it' : `the ${waiting} listeners after it`;
|
|
1591
|
+
this.logger.warn(`${name} took ${(took / 1000).toFixed(1)} s on a message, and held back ${held}. ` + "@MeoCord({ messages: { handlers: 'concurrent' } }) runs them side by side; messages: { slowHandlerWarning: false } turns this off.");
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1374
1594
|
* Answers a message that asks the built-in help, when `messages.help` is on and no handler took the message:
|
|
1375
1595
|
* through the presenter's `messageHelp` when it has one, else in plain text, once the app's guards allow it.
|
|
1376
1596
|
* `known` is the message's starts when they hold the app's prefixes already. Whether it answered, or a
|
|
@@ -1491,6 +1711,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1491
1711
|
this.controllerInstancesCache = new Map();
|
|
1492
1712
|
/** Handler and name pairs already warned about, so a collision warns once, not on every submit or selection. */ this.warnedCollisions = new Set();
|
|
1493
1713
|
/** The handlers already warned about, so each is named once however often it runs. */ this.warnedUnanswered = new Set();
|
|
1714
|
+
/** The message handlers already warned about as slow, so each is named once. */ this.warnedSlow = new Set();
|
|
1494
1715
|
this.container = options.container;
|
|
1495
1716
|
this.controllerClasses = options.controllerClasses;
|
|
1496
1717
|
this.messageOptions = options.messageOptions ?? {};
|
|
@@ -1582,6 +1803,24 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1582
1803
|
for (const [stage, decorator] of found)decorators.set(stage, decorator);
|
|
1583
1804
|
}
|
|
1584
1805
|
}
|
|
1806
|
+
/** Whether making `cls` reaches the per-call `ExecutionContext` through the classes it injects, which each call makes. */ function reachesContext(cls, providers$1, seen = new Set()) {
|
|
1807
|
+
seen.add(cls);
|
|
1808
|
+
return providers.injectedTokens(cls).some((token)=>token === controller_decorator.ExecutionContext || providers.isAppClassToken(token) && !providers$1.has(token) && !seen.has(token) && reachesContext(token, providers$1, seen));
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
|
|
1812
|
+
* app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
|
|
1813
|
+
* made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
|
|
1814
|
+
* is not made.
|
|
1815
|
+
*/ function stageDependencies({ decorators }, providers$1, presenter) {
|
|
1816
|
+
const stages = [
|
|
1817
|
+
...decorators
|
|
1818
|
+
].filter(([, decorator])=>decorator !== '@Controller()').map(([stage])=>stage);
|
|
1819
|
+
if (typeof presenter === 'function') stages.push(presenter);
|
|
1820
|
+
return [
|
|
1821
|
+
...new Set(stages.flatMap((stage)=>providers.injectedTokens(stage)))
|
|
1822
|
+
].filter(providers.isAppClassToken).filter((cls)=>!reachesContext(cls, providers$1));
|
|
1823
|
+
}
|
|
1585
1824
|
/** How a container checks a guard first met at a call: the guard and what it injects, as the startup checks would. */ function lateGuardCheck(providers$1, translator) {
|
|
1586
1825
|
return (guard)=>assertStartupClasses({
|
|
1587
1826
|
classes: providers.reachableClasses([
|
|
@@ -1600,12 +1839,13 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1600
1839
|
/** Runs the startup checks over every class an app runs, so a class that cannot be made is refused by name now. */ function assertStartupClasses({ classes, decorators }, { translator }) {
|
|
1601
1840
|
providers.assertTypedParameters(classes, decorators);
|
|
1602
1841
|
if (translator) return;
|
|
1603
|
-
const needsOne
|
|
1604
|
-
if (needsOne) throw controller_decorator.refuse(controller_decorator.missingTranslatorError(needsOne));
|
|
1842
|
+
for (const needsOne of classes.filter((cls)=>providers.injectedTokens(cls).includes(controller_decorator.Translator)))controller_decorator.startupError(controller_decorator.missingTranslatorError(needsOne));
|
|
1605
1843
|
}
|
|
1606
1844
|
|
|
1607
1845
|
exports.DEFAULT_SHUTDOWN_TIMEOUT_MS = DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
1608
1846
|
exports.Dispatcher = Dispatcher;
|
|
1847
|
+
exports.ReadyPass = ReadyPass;
|
|
1848
|
+
exports.RunningCalls = RunningCalls;
|
|
1609
1849
|
exports.SHUTDOWN_MARGIN_MS = SHUTDOWN_MARGIN_MS;
|
|
1610
1850
|
exports.assertDistinctCommands = assertDistinctCommands;
|
|
1611
1851
|
exports.assertStartupClasses = assertStartupClasses;
|
|
@@ -1622,11 +1862,13 @@ exports.matchComponentRoute = matchComponentRoute;
|
|
|
1622
1862
|
exports.messageDecorator = messageDecorator;
|
|
1623
1863
|
exports.ownInteractionListener = ownInteractionListener;
|
|
1624
1864
|
exports.reactionDecorator = reactionDecorator;
|
|
1865
|
+
exports.readCustomId = readCustomId;
|
|
1625
1866
|
exports.replyWithUserError = replyWithUserError;
|
|
1626
1867
|
exports.runReadyHooks = runReadyHooks;
|
|
1627
1868
|
exports.runShutdownSequence = runShutdownSequence;
|
|
1628
1869
|
exports.shutdownTimeoutOf = shutdownTimeoutOf;
|
|
1629
1870
|
exports.shutdownTimeoutProblem = shutdownTimeoutProblem;
|
|
1871
|
+
exports.stageDependencies = stageDependencies;
|
|
1630
1872
|
exports.startupClasses = startupClasses;
|
|
1631
1873
|
exports.warnHandlersOffControllers = warnHandlersOffControllers;
|
|
1632
1874
|
exports.warnInheritedRoutes = warnInheritedRoutes;
|