meocord 4.1.2 → 4.2.0
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 +98 -0
- package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-CRn9kWob.cjs} +295 -87
- package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-ChvMqnLd.cjs} +74 -40
- package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-BdAUTdYs.cjs} +1 -1
- package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CQo8mSP8.cjs} +158 -72
- package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-DhDVbT5M.cjs} +12 -3
- package/dist/cjs/common/index.cjs +4 -4
- package/dist/cjs/core/index.cjs +195 -157
- package/dist/cjs/decorator/index.cjs +66 -57
- package/dist/cjs/testing/index.cjs +733 -301
- 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/build/load-config.pre-entry.js +4 -1
- package/dist/esm/common/decorator.js +6 -6
- package/dist/esm/common/deprecation.js +2 -2
- package/dist/esm/common/errors.js +2 -2
- package/dist/esm/common/execution-context.js +1 -1
- package/dist/esm/common/metadata.js +1 -1
- package/dist/esm/common/route.js +10 -1
- package/dist/esm/common/translate-error.js +1 -1
- package/dist/esm/core/command-conflicts.js +34 -34
- package/dist/esm/core/component-routes.js +74 -31
- package/dist/esm/core/dispatcher.js +51 -7
- package/dist/esm/core/filter-runner.js +2 -2
- 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/meocord-factory.js +196 -157
- package/dist/esm/core/message-routes.js +4 -3
- package/dist/esm/core/observer-runner.js +4 -3
- package/dist/esm/core/providers.js +38 -21
- package/dist/esm/core/route-specificity.js +4 -4
- package/dist/esm/core/scalar-types.js +15 -3
- package/dist/esm/core/shard-context.js +1 -1
- package/dist/esm/core/startup-roots.js +2 -3
- package/dist/esm/core/theme-runtime.js +2 -2
- 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 +110 -67
- package/dist/esm/decorator/cooldown.decorator.js +3 -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 +8 -8
- 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 +1 -1
- package/dist/esm/testing/cooldown-store-suite.js +18 -10
- package/dist/esm/testing/discord-defaults.js +60 -7
- 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 +229 -188
- package/dist/esm/testing/mock-fn.js +80 -10
- package/dist/esm/testing/mock-interaction.js +223 -72
- package/dist/esm/testing/response.js +1 -1
- package/dist/esm/testing/routing.js +39 -14
- package/dist/esm/testing/startup-errors.js +26 -0
- package/dist/esm/testing/strict-mocks.js +45 -0
- package/dist/esm/testing/theme.js +1 -1
- package/dist/esm/util/bundle-entry.util.js +2 -1
- package/dist/esm/util/interaction.util.js +1 -1
- package/dist/esm/util/meocord-config-validation.util.js +1 -0
- package/dist/esm/util/metadata-keys.js +1 -0
- package/dist/esm/util/refusal.util.js +148 -8
- 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 +8 -6
- package/dist/types/core/index.d.ts +8 -6
- package/dist/types/decorator/index.d.cts +30 -29
- package/dist/types/decorator/index.d.ts +30 -29
- package/dist/types/{index-D7n0FBMm.d.ts → index-CdXsFgjE.d.ts} +62 -15
- package/dist/types/{index-DEP0NYrU.d.cts → index-nn5p7OoX.d.cts} +62 -15
- 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-BZgFoNeB.d.cts} +5 -2
- package/dist/types/{route-C-aMI7Sl.d.ts → route-BZgFoNeB.d.ts} +5 -2
- package/dist/types/testing/index.d.cts +245 -31
- package/dist/types/testing/index.d.ts +245 -31
- package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-BcOJVzR9.d.ts} +1 -1
- package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-UKr3-6Ys.d.cts} +1 -1
- package/dist/types/{translator-J6tIUfdp.d.ts → translator-BViWKtLJ.d.ts} +1 -1
- package/dist/types/{translator-B4mc1lzJ.d.cts → translator-DHMkkCpg.d.cts} +1 -1
- package/package.json +3 -2
|
@@ -1,13 +1,39 @@
|
|
|
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-CRn9kWob.cjs');
|
|
5
|
+
var providers = require('./providers-ChvMqnLd.cjs');
|
|
6
6
|
var enum_index = require('../enum/index.cjs');
|
|
7
|
-
var translateError = require('./translate-error-
|
|
7
|
+
var translateError = require('./translate-error-DhDVbT5M.cjs');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* How a pattern ranks, one character per segment twice over: `L` for literal text or `P` for a param, then how narrow
|
|
11
|
+
* each param is, the narrowest as the lowest digit. Two keys of one length compare as text: literal before param at
|
|
12
|
+
* the first segment where they differ, then the narrower type.
|
|
13
|
+
*/ function rankKey(pattern) {
|
|
14
|
+
const narrowness = pattern.split('/').map(controller_decorator.paramNarrowness);
|
|
15
|
+
const kinds = narrowness.map((rank)=>rank === undefined ? 'L' : 'P').join('');
|
|
16
|
+
return `${kinds}/${narrowness.map((rank)=>String(rank === undefined ? 0 : 9 - rank)).join('')}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Orders two routes as dispatch tries them. Left to right, the first segment one pattern spells out as literal text
|
|
20
|
+
* while the other leaves it to a param ranks the literal one first: `a/{x}` before `{x}/abcd`, `profile/me/{section}`
|
|
21
|
+
* before `profile/{userId}/edit`. Between patterns that leaves tied, the first param where one type is narrower ranks
|
|
22
|
+
* it first: words to choose from, then `bool`, `uuid`, `snowflake`, `int`, `number`, then text. Patterns of different
|
|
23
|
+
* segment counts, which no customId matches both of, are ordered by count only so the order is total; component
|
|
24
|
+
* patterns have no catch-all param. What remains tied keeps its listing order.
|
|
25
|
+
*/ function rank(keys) {
|
|
26
|
+
return (a, b)=>{
|
|
27
|
+
const [left, right] = [
|
|
28
|
+
keys.get(a),
|
|
29
|
+
keys.get(b)
|
|
30
|
+
];
|
|
31
|
+
if (left.length !== right.length) return left.length - right.length;
|
|
32
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every customId-pattern route of the given controllers, in the order dispatch tries them, read from metadata alone.
|
|
11
37
|
* Throws for two handlers whose patterns match the same customIds of one component type; one handler
|
|
12
38
|
* declared under two spellings of a pattern keeps one route.
|
|
13
39
|
*/ function buildComponentRoutes(controllerClasses) {
|
|
@@ -40,22 +66,11 @@ var translateError = require('./translate-error-BnM8FbSH.cjs');
|
|
|
40
66
|
}
|
|
41
67
|
}
|
|
42
68
|
}
|
|
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;
|
|
69
|
+
// A stable sort, so what the ranking leaves tied keeps the order controllers and handlers are listed in
|
|
70
|
+
return routes.sort(rank(new Map(routes.map((route)=>[
|
|
71
|
+
route,
|
|
72
|
+
rankKey(route.pattern)
|
|
73
|
+
]))));
|
|
59
74
|
}
|
|
60
75
|
/** A component type as an error names it: `button`, `modal submit`, `select menu`. */ const typeLabel = (type)=>type.toLowerCase().replaceAll('_', ' ');
|
|
61
76
|
/**
|
|
@@ -80,6 +95,12 @@ const typesByPattern = new Map();
|
|
|
80
95
|
if (!types) typesByPattern.set(pattern, types = controller_decorator.createRegexFromPattern(pattern).types);
|
|
81
96
|
return types;
|
|
82
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* The params `pattern`, compiled as `regex`, captures from a customId, each typed param as its value:
|
|
100
|
+
* `undefined` when the customId does not match, or a segment is not a value of its param's type.
|
|
101
|
+
*/ function readCustomId(pattern, regex, customId) {
|
|
102
|
+
return readCustomIdSegments(pattern, regex, customId)?.values;
|
|
103
|
+
}
|
|
83
104
|
/** {@link readCustomId}'s values, with each param's text as the customId gave it. */ function readCustomIdSegments(pattern, regex, customId) {
|
|
84
105
|
const match = regex.exec(customId);
|
|
85
106
|
if (!match) return undefined;
|
|
@@ -97,18 +118,40 @@ const typesByPattern = new Map();
|
|
|
97
118
|
text
|
|
98
119
|
};
|
|
99
120
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Pattern pairs of one component type that the ranking leaves tied and that can match one customId, each with the
|
|
123
|
+
* pattern that runs, the one ranked first in `routes`, and an id both match. Only patterns of one rank key, read
|
|
124
|
+
* together, can tie, so each group is compared within itself.
|
|
125
|
+
*/ function findComponentRouteConflicts(routes) {
|
|
126
|
+
const groups = new Map();
|
|
127
|
+
for (const route of routes){
|
|
128
|
+
// The literal text and each param's narrowness: patterns that differ in either rank apart or match no id in common
|
|
129
|
+
const key = `${route.meta.type}\0${route.pattern.split('/').map((segment)=>controller_decorator.paramNarrowness(segment) ?? `=${segment}`).join('\0')}`;
|
|
130
|
+
groups.set(key, [
|
|
131
|
+
...groups.get(key) ?? [],
|
|
132
|
+
route
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
const conflicts = [];
|
|
136
|
+
for (const group of groups.values()){
|
|
137
|
+
for(let i = 0; i < group.length; i++){
|
|
138
|
+
for(let j = i + 1; j < group.length; j++){
|
|
139
|
+
const customId = controller_decorator.sharedCustomId(group[i].pattern, group[j].pattern);
|
|
140
|
+
if (customId === undefined) continue;
|
|
141
|
+
const patterns = [
|
|
142
|
+
group[i].pattern,
|
|
143
|
+
group[j].pattern
|
|
144
|
+
];
|
|
145
|
+
conflicts.push({
|
|
146
|
+
type: group[i].meta.type,
|
|
147
|
+
patterns,
|
|
148
|
+
runs: group[i].pattern,
|
|
149
|
+
customId
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return conflicts;
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.methodName}`;
|
|
@@ -131,7 +174,7 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
|
|
|
131
174
|
* Refuses commands of which only one could run or be registered: two handlers of one slash command name or subcommand
|
|
132
175
|
* 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
176
|
* message context menu of one name, are no clash. The app, `meocord register`, the shard manager and testing call it.
|
|
134
|
-
*
|
|
177
|
+
* Each clash is a startup error, naming both handlers or both builders with what to do instead; see `startupError`.
|
|
135
178
|
*/ function assertDistinctCommands(controllerClasses) {
|
|
136
179
|
const handlers = new Map();
|
|
137
180
|
const builders = new Map();
|
|
@@ -149,7 +192,8 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
|
|
|
149
192
|
const key = `${meta.type}\0${name}`;
|
|
150
193
|
const earlier = (handlers.get(key) ?? []).find((other)=>overlap(other.meta, meta));
|
|
151
194
|
if (earlier) {
|
|
152
|
-
|
|
195
|
+
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.`));
|
|
196
|
+
continue;
|
|
153
197
|
}
|
|
154
198
|
handlers.set(key, [
|
|
155
199
|
...handlers.get(key) ?? [],
|
|
@@ -170,7 +214,7 @@ const where = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.metho
|
|
|
170
214
|
name: typeof body.name === 'string' ? body.name : name
|
|
171
215
|
});
|
|
172
216
|
} else if (built.meta.builderClass !== meta.builderClass) {
|
|
173
|
-
|
|
217
|
+
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
218
|
}
|
|
175
219
|
}
|
|
176
220
|
}
|
|
@@ -348,18 +392,21 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
|
|
|
348
392
|
/**
|
|
349
393
|
* Warns about a handler a subclass re-declares on other routes while it still answers the ones it inherits, for
|
|
350
394
|
* `@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
|
|
395
|
+
* the inherited routes. Every class a listed controller extends is checked, a base that is not listed included, for
|
|
396
|
+
* the methods the controller still inherits through it.
|
|
352
397
|
*/ function warnInheritedRoutes(controllerClasses) {
|
|
353
|
-
|
|
354
|
-
const problems =
|
|
398
|
+
// A set, so a class two listed controllers extend is named once, for the routes either of them still answers
|
|
399
|
+
const problems = new Set();
|
|
355
400
|
for (const controllerClass of controllerClasses){
|
|
401
|
+
// Methods a class below re-declares with @Controller({ inheritedRoutes: 'replace' }): the listed controller inherits
|
|
402
|
+
// nothing of them from above it, so a class above is not named for them
|
|
403
|
+
const cut = new Set();
|
|
356
404
|
for(let prototype = controllerClass.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
|
|
357
|
-
if (checked.has(prototype)) break;
|
|
358
|
-
checked.add(prototype);
|
|
359
405
|
const declared = controller_decorator.getDeclaredRoutes(prototype);
|
|
360
406
|
const base = Object.getPrototypeOf(prototype);
|
|
361
407
|
if (declared.length === 0 || !base) continue;
|
|
362
|
-
const
|
|
408
|
+
if (Reflect.getOwnMetadata(controller_decorator.META.replacesInheritedRoutes, prototype)) for (const route of declared)cut.add(route.method);
|
|
409
|
+
const inherited = controller_decorator.getHandlerRoutes(base).filter((route)=>!cut.has(route.method));
|
|
363
410
|
for (const method of new Set(declared.map((route)=>route.method))){
|
|
364
411
|
const own = [
|
|
365
412
|
...new Set(declared.filter((route)=>route.method === method).map((route)=>route.label))
|
|
@@ -369,17 +416,19 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
|
|
|
369
416
|
].filter((label)=>!own.includes(label));
|
|
370
417
|
if (kept.length === 0) continue;
|
|
371
418
|
const name = prototype.constructor.name;
|
|
372
|
-
problems.
|
|
419
|
+
problems.add(` ${name}.${method} answers ${controller_decorator.listed(kept)}, which it inherits, as well as its own ${controller_decorator.listed(own)}.`);
|
|
373
420
|
}
|
|
374
421
|
}
|
|
375
422
|
}
|
|
376
|
-
if (problems.
|
|
377
|
-
const one = problems.
|
|
378
|
-
logger.warn(`${problems.
|
|
423
|
+
if (problems.size === 0) return;
|
|
424
|
+
const one = problems.size === 1;
|
|
425
|
+
logger.warn(`${problems.size} re-declared ${one ? 'handler still answers routes it inherits' : 'handlers still answer routes they inherit'}:\n` + `${[
|
|
426
|
+
...problems
|
|
427
|
+
].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
428
|
}
|
|
380
429
|
/**
|
|
381
|
-
* Warns of customId patterns of one component type that can match one customId, naming
|
|
382
|
-
*
|
|
430
|
+
* Warns of customId patterns of one component type that rank equally and can match one customId, naming an id both
|
|
431
|
+
* match and the handler that runs. An app with such a pair works, so it starts; the next major version (5.0) refuses.
|
|
383
432
|
* @throws Error for two handlers whose patterns match the same customIds, which the app refuses as well.
|
|
384
433
|
*/ function warnOverlappingPatterns(controllerClasses) {
|
|
385
434
|
const routes = buildComponentRoutes(controllerClasses);
|
|
@@ -389,23 +438,16 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
|
|
|
389
438
|
`${route.meta.type}\0${route.pattern}`,
|
|
390
439
|
route
|
|
391
440
|
]));
|
|
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
441
|
const name = ({ controllerClass, meta })=>`${controllerClass.name}.${meta.methodName}`;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
442
|
+
const lines = conflicts.map(({ type, patterns: [left, right], customId })=>{
|
|
443
|
+
const [runs, other] = [
|
|
444
|
+
routeOf.get(`${type}\0${left}`),
|
|
445
|
+
routeOf.get(`${type}\0${right}`)
|
|
446
|
+
];
|
|
447
|
+
const listed = runs.controllerClass === other.controllerClass ? 'it is declared first' : 'its controller is listed first';
|
|
448
|
+
return ` "${left}" vs "${right}", which both match "${customId}": ${name(runs)} runs, as ${listed}.`;
|
|
449
|
+
});
|
|
450
|
+
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
451
|
}
|
|
410
452
|
|
|
411
453
|
const UNKNOWN_INTERACTION = 10062;
|
|
@@ -1100,6 +1142,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1100
1142
|
if (state && state !== 'unanswered') return true;
|
|
1101
1143
|
return 'replied' in interaction && (interaction.replied || interaction.deferred);
|
|
1102
1144
|
}
|
|
1145
|
+
/** 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
1146
|
/**
|
|
1104
1147
|
* Routes interactions, messages and reactions to the handlers that take them, as the bot does, and runs
|
|
1105
1148
|
* each through its pipeline. The app and the testing module share it, so a test routes exactly as the bot.
|
|
@@ -1355,22 +1398,64 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1355
1398
|
message
|
|
1356
1399
|
], error, this.runOptions(call));
|
|
1357
1400
|
}
|
|
1401
|
+
// The matched handler, then every listener, each resolved from the container only as it starts
|
|
1402
|
+
const runs = [];
|
|
1358
1403
|
if (target) {
|
|
1359
1404
|
const { route, params, start, given } = target;
|
|
1360
1405
|
noteInvocation(message, `${start}${providers.commandWordsOf(route.tokens).join(' ')}`);
|
|
1361
1406
|
const hooks = providers.messageCommandHooks(route, params, message, start, given, this.messageOptions.types);
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1407
|
+
runs.push({
|
|
1408
|
+
name: `${route.controllerClass.name}.${route.method}`,
|
|
1409
|
+
run: (at)=>this.invokeHandler(this.getInstance(route.controllerClass), route.method, [
|
|
1410
|
+
message,
|
|
1411
|
+
params
|
|
1412
|
+
], at, hooks)
|
|
1413
|
+
});
|
|
1366
1414
|
}
|
|
1367
1415
|
for (const { controllerClass, method } of this.messageListeners){
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1416
|
+
runs.push({
|
|
1417
|
+
name: `${controllerClass.name}.${method}`,
|
|
1418
|
+
run: (at)=>this.invokeHandler(this.getInstance(controllerClass), method, [
|
|
1419
|
+
message
|
|
1420
|
+
], at)
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
if (this.messageOptions.handlers === 'concurrent') await this.runTogether(runs, call);
|
|
1424
|
+
else await this.runInTurn(runs, call);
|
|
1425
|
+
}
|
|
1426
|
+
/** Runs a message's handlers one after another, warning once about one that held the rest back for long. */ async runInTurn(runs, call) {
|
|
1427
|
+
const warn = this.messageOptions.slowHandlerWarning ?? this.options.warnSlowMessageHandlers ?? true;
|
|
1428
|
+
for (const [index, { name, run }] of runs.entries()){
|
|
1429
|
+
const started = performance.now();
|
|
1430
|
+
await run(call);
|
|
1431
|
+
const waiting = runs.length - 1 - index;
|
|
1432
|
+
if (warn && waiting > 0) this.warnSlowOnce(name, performance.now() - started, waiting);
|
|
1371
1433
|
}
|
|
1372
1434
|
}
|
|
1373
1435
|
/**
|
|
1436
|
+
* Starts a message's handlers together and settles once all have. Each is reported as settled in the order the
|
|
1437
|
+
* handlers are listed, whichever settles first, and a call that rejects, rejects this once every one has settled.
|
|
1438
|
+
*/ async runTogether(runs, call) {
|
|
1439
|
+
const { record } = call;
|
|
1440
|
+
const reports = runs.map(()=>[]);
|
|
1441
|
+
const results = await Promise.allSettled(runs.map(({ run }, index)=>run(record ? {
|
|
1442
|
+
...call,
|
|
1443
|
+
record: {
|
|
1444
|
+
settled: (...report)=>reports[index].push(report),
|
|
1445
|
+
unhandled: (error)=>record.unhandled(error)
|
|
1446
|
+
}
|
|
1447
|
+
} : call)));
|
|
1448
|
+
for (const report of reports.flat())record?.settled(...report);
|
|
1449
|
+
const failed = results.find((result)=>result.status === 'rejected');
|
|
1450
|
+
if (failed) throw failed.reason;
|
|
1451
|
+
}
|
|
1452
|
+
/** Warns, once per handler, that it took `SLOW_HANDLER_MS` or longer with `waiting` handlers held back behind it. */ warnSlowOnce(name, took, waiting) {
|
|
1453
|
+
if (took < SLOW_HANDLER_MS || this.warnedSlow.has(name)) return;
|
|
1454
|
+
this.warnedSlow.add(name);
|
|
1455
|
+
const held = waiting === 1 ? 'the listener after it' : `the ${waiting} listeners after it`;
|
|
1456
|
+
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.");
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1374
1459
|
* Answers a message that asks the built-in help, when `messages.help` is on and no handler took the message:
|
|
1375
1460
|
* through the presenter's `messageHelp` when it has one, else in plain text, once the app's guards allow it.
|
|
1376
1461
|
* `known` is the message's starts when they hold the app's prefixes already. Whether it answered, or a
|
|
@@ -1491,6 +1576,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1491
1576
|
this.controllerInstancesCache = new Map();
|
|
1492
1577
|
/** Handler and name pairs already warned about, so a collision warns once, not on every submit or selection. */ this.warnedCollisions = new Set();
|
|
1493
1578
|
/** The handlers already warned about, so each is named once however often it runs. */ this.warnedUnanswered = new Set();
|
|
1579
|
+
/** The message handlers already warned about as slow, so each is named once. */ this.warnedSlow = new Set();
|
|
1494
1580
|
this.container = options.container;
|
|
1495
1581
|
this.controllerClasses = options.controllerClasses;
|
|
1496
1582
|
this.messageOptions = options.messageOptions ?? {};
|
|
@@ -1600,8 +1686,7 @@ async function replyToMessage(message, error, logger, withEmoji, answering) {
|
|
|
1600
1686
|
/** 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
1687
|
providers.assertTypedParameters(classes, decorators);
|
|
1602
1688
|
if (translator) return;
|
|
1603
|
-
const needsOne
|
|
1604
|
-
if (needsOne) throw controller_decorator.refuse(controller_decorator.missingTranslatorError(needsOne));
|
|
1689
|
+
for (const needsOne of classes.filter((cls)=>providers.injectedTokens(cls).includes(controller_decorator.Translator)))controller_decorator.startupError(controller_decorator.missingTranslatorError(needsOne));
|
|
1605
1690
|
}
|
|
1606
1691
|
|
|
1607
1692
|
exports.DEFAULT_SHUTDOWN_TIMEOUT_MS = DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
@@ -1622,6 +1707,7 @@ exports.matchComponentRoute = matchComponentRoute;
|
|
|
1622
1707
|
exports.messageDecorator = messageDecorator;
|
|
1623
1708
|
exports.ownInteractionListener = ownInteractionListener;
|
|
1624
1709
|
exports.reactionDecorator = reactionDecorator;
|
|
1710
|
+
exports.readCustomId = readCustomId;
|
|
1625
1711
|
exports.replyWithUserError = replyWithUserError;
|
|
1626
1712
|
exports.runReadyHooks = runReadyHooks;
|
|
1627
1713
|
exports.runShutdownSequence = runShutdownSequence;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var controller_decorator = require('./controller.decorator-
|
|
3
|
+
var controller_decorator = require('./controller.decorator-CRn9kWob.cjs');
|
|
4
4
|
var discord_js = require('discord.js');
|
|
5
5
|
|
|
6
6
|
/** The longest customId Discord accepts. */ const MAX_CUSTOM_ID_LENGTH = 100;
|
|
@@ -16,6 +16,10 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
16
16
|
decodeSegment(value)
|
|
17
17
|
]));
|
|
18
18
|
}
|
|
19
|
+
/** The segment types whose value is text, which `build` takes only as a string. */ const TEXT_TYPES = new Set([
|
|
20
|
+
'snowflake',
|
|
21
|
+
'uuid'
|
|
22
|
+
]);
|
|
19
23
|
/**
|
|
20
24
|
* Makes a typed route from a customId pattern, so one declaration serves the handler and the ids that reach it.
|
|
21
25
|
*
|
|
@@ -50,7 +54,7 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
50
54
|
*
|
|
51
55
|
* @group Utilities
|
|
52
56
|
* @see {@link Route}
|
|
53
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
57
|
+
* @see {@link https://meocord.dev/docs/4.2/components | Buttons, selects and modals}
|
|
54
58
|
*/ function route(pattern) {
|
|
55
59
|
const { params, types } = controller_decorator.createRegexFromPattern(pattern);
|
|
56
60
|
const names = new Set(params);
|
|
@@ -62,6 +66,11 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
62
66
|
const value = Object.hasOwn(values, name) ? values[name] : undefined;
|
|
63
67
|
const type = Object.hasOwn(types, name) ? types[name] : undefined;
|
|
64
68
|
if (value === undefined || value === null) throw new TypeError(`route('${pattern}').build() needs a value for ${placeholder}.`);
|
|
69
|
+
// A snowflake's or uuid's value is its text: a number for a snowflake may have lost digits before it got here
|
|
70
|
+
if (type && TEXT_TYPES.has(type) && typeof value !== 'string') {
|
|
71
|
+
const lost = type === 'snowflake' ? ' A number may have lost digits already: pass the ID as text, such as user.id.' : '';
|
|
72
|
+
throw new TypeError(`route('${pattern}').build() got the ${typeof value} ${String(value)} for ${placeholder}, which takes its value as a string.${lost}`);
|
|
73
|
+
}
|
|
65
74
|
const text = String(value);
|
|
66
75
|
if (text === '') throw new TypeError(`route('${pattern}').build() got an empty {${name}}, which no customId segment can hold.`);
|
|
67
76
|
// A typed segment must read back as the value it was built from, or the route could never match it
|
|
@@ -139,7 +148,7 @@ const PLACEHOLDER = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
139
148
|
* @group Utilities
|
|
140
149
|
* @category Localisation
|
|
141
150
|
* @see {@link createTranslator}
|
|
142
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
151
|
+
* @see {@link https://meocord.dev/docs/4.2/localisation | Localisation}
|
|
143
152
|
*/ function translateError(error, t, target) {
|
|
144
153
|
const locale = target instanceof discord_js.BaseInteraction ? controller_decorator.interactionLocale(target) : target instanceof discord_js.Message ? controller_decorator.messageLocale(target) : target;
|
|
145
154
|
return errorText(error, t, locale);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var controller_decorator = require('../_shared/controller.decorator-
|
|
3
|
+
var controller_decorator = require('../_shared/controller.decorator-CRn9kWob.cjs');
|
|
4
4
|
require('reflect-metadata');
|
|
5
|
-
var translateError = require('../_shared/translate-error-
|
|
6
|
-
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-
|
|
5
|
+
var translateError = require('../_shared/translate-error-DhDVbT5M.cjs');
|
|
6
|
+
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-BdAUTdYs.cjs');
|
|
7
7
|
require('../enum/index.cjs');
|
|
8
8
|
require('discord.js');
|
|
9
9
|
require('path');
|
|
@@ -150,7 +150,7 @@ const assigned = {};
|
|
|
150
150
|
*
|
|
151
151
|
* @group Utilities
|
|
152
152
|
* @see {@link ExecutionContext}
|
|
153
|
-
* @see {@link https://meocord.dev/docs/4.
|
|
153
|
+
* @see {@link https://meocord.dev/docs/4.2/guards | Guards}
|
|
154
154
|
*/ function createMetadata(description) {
|
|
155
155
|
const key = Symbol(description);
|
|
156
156
|
const decorator = (value)=>function(target, propertyKey) {
|