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
|
@@ -27,20 +27,21 @@ import { bunDevelopmentValues, bunDevelopmentWarning } from '../util/inherited-e
|
|
|
27
27
|
import { assertBuiltForThisPlatform } from '../util/platform.util.js';
|
|
28
28
|
import { isRegisterOnly } from '../util/registration-mode.util.js';
|
|
29
29
|
import { injectedTokens, singletonContextError } from './guard-runner.js';
|
|
30
|
-
import { appStages, bindGlobalStages, prepareHandlerStages, bindAppPresenter } from './handler-pipeline.js';
|
|
30
|
+
import { appStages, bindGlobalStages, prepareHandlerStages, prepareThemes, bindAppPresenter } from './handler-pipeline.js';
|
|
31
31
|
import { startupClasses, assertStartupClasses, lateGuardCheck } from './startup-roots.js';
|
|
32
32
|
import { markStartupChecked, assertStartupChecked } from './startup-checked.js';
|
|
33
|
+
import { makeInjectable } from '../util/injectable.util.js';
|
|
33
34
|
import { meocordClasses } from './meocord-classes.js';
|
|
35
|
+
import { buildMessageRoutes } from './message-routes.js';
|
|
34
36
|
import { appObservers, bindObservers } from './observer-runner.js';
|
|
35
|
-
import { makeInjectable } from '../util/injectable.util.js';
|
|
36
37
|
import { isAppClassToken } from './lifecycle-order.js';
|
|
37
|
-
import {
|
|
38
|
+
import { reachableClasses, providerMap, bindsOwnToken, tokenName, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies, resolveProviders } from './providers.js';
|
|
38
39
|
import { HandlerRegistry } from './handler-registry.js';
|
|
39
40
|
import { ShardManager } from './shard-manager.js';
|
|
40
|
-
import {
|
|
41
|
+
import { warnUnregisteredCommands, warnInheritedRoutes, warnOverlappingPatterns, warnHandlersOffControllers, assertDistinctCommands } from './command-conflicts.js';
|
|
41
42
|
import { ShardContext, SHARD_CALL_KEY, shardCallHandler } from './shard-context.js';
|
|
42
43
|
import { registerClientTheme } from './theme-runtime.js';
|
|
43
|
-
import { isRefusal, describeRefusal, refuse } from '../util/refusal.util.js';
|
|
44
|
+
import { isRefusal, logStartupErrors, describeRefusal, allDeclaredErrors, declaredErrorsOf, collectStartupErrors, refuse, startupError, stopOnStartupErrors } from '../util/refusal.util.js';
|
|
44
45
|
import { endFailedShard } from './shard-exit.js';
|
|
45
46
|
import { isBuiltApplication } from '../util/bundle-entry.util.js';
|
|
46
47
|
import { META } from '../util/metadata-keys.js';
|
|
@@ -51,14 +52,14 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
51
52
|
*/ function bindDependencies(container, cls, providers) {
|
|
52
53
|
// A provided class is bound by its own provider, wherever in the list that provider comes
|
|
53
54
|
if (container.isBound(cls) || providers.has(cls)) return;
|
|
54
|
-
if (injectedTokens(cls).includes(ExecutionContext))
|
|
55
|
+
if (injectedTokens(cls).includes(ExecutionContext)) return startupError(singletonContextError(cls));
|
|
55
56
|
assertStartupChecked(container, cls);
|
|
56
57
|
makeInjectable(cls);
|
|
57
58
|
container.bind(cls).toSelf().inSingletonScope();
|
|
58
59
|
// By constructor type or @inject token; an interface-typed parameter records Object, which is skipped
|
|
59
60
|
for (const dep of injectedTokens(cls)){
|
|
60
|
-
if (dep === Translator && !container.isBound(Translator))
|
|
61
|
-
if (isAppClassToken(dep)) bindDependencies(container, dep, providers);
|
|
61
|
+
if (dep === Translator && !container.isBound(Translator)) startupError(missingTranslatorError(cls));
|
|
62
|
+
else if (isAppClassToken(dep)) bindDependencies(container, dep, providers);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
@@ -124,34 +125,51 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
124
125
|
* load, naming the file and the reason, with what to do about it, when a provider cannot
|
|
125
126
|
* be bound, such as one for a token MeoCord binds itself, when two handlers take one command, or two builder
|
|
126
127
|
* classes build one, naming both, and for any other mistake it refuses as the app loads, such as two component
|
|
127
|
-
* patterns that match the same customIds.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* patterns that match the same customIds. Each check reports every error it finds, and the first is thrown,
|
|
129
|
+
* unchanged. When there are several, each is logged first, with where in the source it comes from; a lone one is
|
|
130
|
+
* logged in a built application. Either way `isExplainedError()` then tells a caller not to log it again; in a
|
|
131
|
+
* shard of process sharding, the manager logs it. With `startupErrors: 'all'` in `meocord.config.ts`, the errors
|
|
132
|
+
* decorators kept are among them.
|
|
130
133
|
*/ static create(target) {
|
|
131
134
|
try {
|
|
132
135
|
return this.createApplication(target);
|
|
133
136
|
} catch (error) {
|
|
134
137
|
// A shard ends, so its manager restarts it, or stops every shard for a refusal, which it logs
|
|
135
138
|
if (isShardProcess()) endFailedShard(error);
|
|
136
|
-
else if (
|
|
137
|
-
|
|
138
|
-
markExplained(error);
|
|
139
|
+
else if (isRefusal(error) && !isExplainedError(error)) {
|
|
140
|
+
// Every startup error found together is logged, in a test too; a lone one is logged in a built application
|
|
141
|
+
if (logStartupErrors(error, (text)=>this.logger.error(text), 'the bot did not start')) markExplained(error);
|
|
142
|
+
else if (isBuiltApplication()) {
|
|
143
|
+
this.logger.error(describeRefusal(error, process.cwd()));
|
|
144
|
+
markExplained(error);
|
|
145
|
+
}
|
|
139
146
|
}
|
|
140
147
|
throw error;
|
|
141
148
|
}
|
|
142
149
|
}
|
|
143
150
|
static createApplication(target) {
|
|
144
151
|
const options = Reflect.getMetadata(META.appOptions, target);
|
|
152
|
+
// With startupErrors: 'all', what the decorators kept: a built bot's every one, else the app's and its controllers'
|
|
153
|
+
const declared = isBuiltApplication() ? allDeclaredErrors() : declaredErrorsOf([
|
|
154
|
+
target,
|
|
155
|
+
...options?.controllers ?? []
|
|
156
|
+
]);
|
|
145
157
|
if (!options) {
|
|
158
|
+
// A refused @MeoCord stores no options, so its own errors say why
|
|
159
|
+
collectStartupErrors(()=>declared.forEach(startupError));
|
|
146
160
|
throw refuse(new Error(`${typeof target === 'function' ? target.name : String(target)}: not decorated with @MeoCord(), so there is no app to create.`));
|
|
147
161
|
}
|
|
148
162
|
const meocordConfig = loadMeoCordConfig();
|
|
149
163
|
if (!meocordConfig) {
|
|
150
164
|
throw refuse(new Error(compiledConfigMessage()));
|
|
151
165
|
}
|
|
152
|
-
// Before any of the three ways a bot runs, so none registers or dispatches a command only one handler could take
|
|
153
|
-
|
|
154
|
-
const providers =
|
|
166
|
+
// Before any of the three ways a bot runs, so none registers or dispatches a command only one handler could take; every
|
|
167
|
+
// error each check finds is reported, the first thrown
|
|
168
|
+
const providers = collectStartupErrors(()=>{
|
|
169
|
+
declared.forEach(startupError);
|
|
170
|
+
assertDistinctCommands(options.controllers);
|
|
171
|
+
return providerMap(options.providers ?? [], '@MeoCord({ providers })');
|
|
172
|
+
});
|
|
155
173
|
// A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
|
|
156
174
|
const themeResolver = themeResolverClass(options.themeFor);
|
|
157
175
|
// The app's classes and every class they inject, which the container binds
|
|
@@ -193,153 +211,174 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
193
211
|
config: meocordConfig
|
|
194
212
|
});
|
|
195
213
|
}
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
container.bind(COOLDOWN_POLICY).toConstantValue(cooldownPolicyFrom(options));
|
|
233
|
-
// A store of the app's own is resolved like a service, so it can inject its client
|
|
234
|
-
if (options.cooldownStore) {
|
|
235
|
-
bindDependencies(container, options.cooldownStore, providers);
|
|
236
|
-
container.bind(CooldownStore).toService(options.cooldownStore);
|
|
237
|
-
} else {
|
|
238
|
-
container.bind(CooldownStore).toConstantValue(new MemoryCooldownStore());
|
|
239
|
-
}
|
|
240
|
-
// A class that injects the token depends on the app's store, the one unit whose hooks run
|
|
241
|
-
bindsOwnToken(container, CooldownStore, options.cooldownStore);
|
|
242
|
-
// After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
|
|
243
|
-
// a class token that is provided is not also bound as itself
|
|
244
|
-
for (const [token, provider] of providers){
|
|
245
|
-
if (container.isBound(token)) {
|
|
246
|
-
throw refuse(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${tokenName(token)}, which MeoCord binds itself.`));
|
|
214
|
+
// The checks of what the app binds and runs report every error each finds, and the first is thrown
|
|
215
|
+
return collectStartupErrors(()=>{
|
|
216
|
+
// Every class the app runs, its stages and presenter included, checked before binding, where inversify would
|
|
217
|
+
// otherwise fail first with an error about compiler options, or a guard only at its first call
|
|
218
|
+
const stages = appStages(target);
|
|
219
|
+
const runs = startupClasses({
|
|
220
|
+
controllers: options.controllers,
|
|
221
|
+
services: options.services ?? [],
|
|
222
|
+
providers,
|
|
223
|
+
stages,
|
|
224
|
+
presenter: options.presenter,
|
|
225
|
+
cooldownStore: options.cooldownStore,
|
|
226
|
+
themeFor: options.themeFor,
|
|
227
|
+
observers: appObservers(target)
|
|
228
|
+
});
|
|
229
|
+
assertStartupClasses(runs, {
|
|
230
|
+
translator: options.i18n !== undefined || providers.has(Translator)
|
|
231
|
+
});
|
|
232
|
+
// Outside a built application, the kept errors of the stages and other classes the app's classes reach
|
|
233
|
+
if (!isBuiltApplication()) declaredErrorsOf(runs.classes).forEach(startupError);
|
|
234
|
+
// The checks of the wiring below run on classes that passed these, so a mistake in one is not reported again there
|
|
235
|
+
stopOnStartupErrors();
|
|
236
|
+
// The checks bind into this container and change nothing else: what they bind is made once all have passed
|
|
237
|
+
const container = new Container();
|
|
238
|
+
const translator = options.i18n !== undefined || providers.has(Translator);
|
|
239
|
+
markStartupChecked(container, [
|
|
240
|
+
...runs.classes,
|
|
241
|
+
...meocordClasses()
|
|
242
|
+
], lateGuardCheck(providers, translator));
|
|
243
|
+
bindGlobalStages(container, stages);
|
|
244
|
+
// Bound now, so no provider stands in for it; made once the checks pass, the one Client the app logs in with
|
|
245
|
+
const clientOptions = clientOptionsWithSharding(this.effectiveConfig(meocordConfig), options.clientOptions);
|
|
246
|
+
container.bind(Client).toDynamicValue(()=>new Client(clientOptions)).inSingletonScope();
|
|
247
|
+
if (options.i18n) {
|
|
248
|
+
container.bind(Translator).toConstantValue(options.i18n);
|
|
249
|
+
bindsOwnToken(container, Translator);
|
|
247
250
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
274
|
-
appClasses.push(...order.filter((token)=>{
|
|
275
|
-
const provider = providers.get(token);
|
|
276
|
-
return isAppClassToken(token) && (!provider || isClassProvider(provider) && provider.useClass === token);
|
|
277
|
-
}));
|
|
278
|
-
assertProvided(container, providers, runs.classes, '@MeoCord({ providers })');
|
|
279
|
-
// The store first, after only what it injects: it is ready before anything a call reaches, and shuts down last
|
|
280
|
-
const store = options.cooldownStore;
|
|
281
|
-
const lifecycle = (store ? resolutionOrder(container, providers, [
|
|
282
|
-
store,
|
|
283
|
-
...order
|
|
284
|
-
]) : order).map((token)=>({
|
|
285
|
-
token,
|
|
286
|
-
name: tokenName(token),
|
|
287
|
-
dependencies: tokenDependencies(container, providers, token),
|
|
288
|
-
...token === store && {
|
|
289
|
-
cooldownStore: true
|
|
290
|
-
}
|
|
291
|
-
}));
|
|
292
|
-
// What ShardContext.call reaches: the app's classes, and the classes its providers stand in for
|
|
293
|
-
const callable = [
|
|
294
|
-
...new Set([
|
|
295
|
-
...appClasses,
|
|
296
|
-
...[
|
|
297
|
-
...providers.keys()
|
|
298
|
-
].filter(isAppClassToken)
|
|
299
|
-
])
|
|
300
|
-
];
|
|
301
|
-
// From another shard a call names its class, so with process sharding every name it can reach is one class's
|
|
302
|
-
if (meocordConfig.sharding?.mode === 'process') {
|
|
303
|
-
const names = new Set();
|
|
304
|
-
for (const cls of callable){
|
|
305
|
-
if (names.has(cls.name)) {
|
|
306
|
-
throw refuse(new Error(`${cls.name}: two classes have this name; with process sharding, ShardContext.call finds a class in another ` + 'shard by its name, so give each controller, service and provided class a distinct name.'));
|
|
251
|
+
// Bound before the app's classes, so a class that injects it gets this instance; filled once they are bound
|
|
252
|
+
const appClasses = [];
|
|
253
|
+
container.bind(HandlerRegistry).toConstantValue(new HandlerRegistry(appClasses, options.messages, ()=>options.i18n));
|
|
254
|
+
container.bind(ShardContext).toDynamicValue(()=>{
|
|
255
|
+
const client = container.get(Client);
|
|
256
|
+
return new ShardContext(client, (service, method, args)=>Reflect.get(client, SHARD_CALL_KEY)(service, method, args));
|
|
257
|
+
}).inSingletonScope();
|
|
258
|
+
bindsOwnToken(container, HandlerRegistry);
|
|
259
|
+
bindsOwnToken(container, ShardContext);
|
|
260
|
+
container.bind(COOLDOWN_POLICY).toConstantValue(cooldownPolicyFrom(options));
|
|
261
|
+
// A store of the app's own is resolved like a service, so it can inject its client
|
|
262
|
+
if (options.cooldownStore) {
|
|
263
|
+
bindDependencies(container, options.cooldownStore, providers);
|
|
264
|
+
container.bind(CooldownStore).toService(options.cooldownStore);
|
|
265
|
+
} else {
|
|
266
|
+
container.bind(CooldownStore).toConstantValue(new MemoryCooldownStore());
|
|
267
|
+
}
|
|
268
|
+
// A class that injects the token depends on the app's store, the one unit whose hooks run
|
|
269
|
+
bindsOwnToken(container, CooldownStore, options.cooldownStore);
|
|
270
|
+
// After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
|
|
271
|
+
// a class token that is provided is not also bound as itself
|
|
272
|
+
for (const [token, provider] of providers){
|
|
273
|
+
if (container.isBound(token)) {
|
|
274
|
+
startupError(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${tokenName(token)}, which MeoCord binds itself.`));
|
|
275
|
+
continue;
|
|
307
276
|
}
|
|
308
|
-
|
|
277
|
+
bindProvider(container, provider, (cls)=>bindDependencies(container, cls, providers));
|
|
309
278
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
for (const cls of runs.classes){
|
|
314
|
-
Reflect.defineMetadata(META.container, container, cls);
|
|
315
|
-
}
|
|
316
|
-
prepareHandlerStages(container, appClasses);
|
|
317
|
-
bindObservers(container, observers);
|
|
318
|
-
// Run by start() before it logs in: what may inject a provided value is resolved once every factory
|
|
319
|
-
// has made its value, including those that return a promise
|
|
320
|
-
const logger = this.logger;
|
|
321
|
-
const startup = async ()=>{
|
|
322
|
-
try {
|
|
323
|
-
await resolveProviders(container, providers, order);
|
|
324
|
-
} catch (error) {
|
|
325
|
-
logger.error(`${error.message}. The bot cannot start without it.`);
|
|
326
|
-
logger.debug('Provider failure:', error.cause);
|
|
327
|
-
markExplained(error);
|
|
328
|
-
throw error;
|
|
279
|
+
// Bind all controllers and their transitive dependencies
|
|
280
|
+
for (const ctrl of options.controllers){
|
|
281
|
+
bindDependencies(container, ctrl, providers);
|
|
329
282
|
}
|
|
330
|
-
// The listed services are made now, so constructors that attach listeners or connect run before login
|
|
331
283
|
for (const svc of options.services ?? []){
|
|
332
|
-
container
|
|
284
|
+
bindDependencies(container, svc, providers);
|
|
333
285
|
}
|
|
334
|
-
if (
|
|
335
|
-
|
|
286
|
+
if (themeResolver) bindDependencies(container, themeResolver, providers);
|
|
287
|
+
// Observers are services too: bound here so their lifecycle hooks run in dependency order
|
|
288
|
+
const observers = appObservers(target);
|
|
289
|
+
for (const observer of observers)bindDependencies(container, observer, providers);
|
|
290
|
+
// What follows walks what these bound, so a class refused here is not reported again as missing
|
|
291
|
+
stopOnStartupErrors();
|
|
292
|
+
// Providers first, then the services, the controllers and the observers, each after what it depends on. The app's
|
|
293
|
+
// own classes, which a class injecting CooldownStore does not make of the store
|
|
294
|
+
const order = resolutionOrder(container, providers, [
|
|
295
|
+
...providers.keys(),
|
|
296
|
+
...options.services ?? [],
|
|
297
|
+
...themeResolver ? [
|
|
298
|
+
themeResolver
|
|
299
|
+
] : [],
|
|
300
|
+
...options.controllers,
|
|
301
|
+
...observers
|
|
302
|
+
], {
|
|
303
|
+
followOwnTokens: false
|
|
304
|
+
});
|
|
305
|
+
appClasses.push(...order.filter((token)=>{
|
|
306
|
+
const provider = providers.get(token);
|
|
307
|
+
return isAppClassToken(token) && (!provider || isClassProvider(provider) && provider.useClass === token);
|
|
308
|
+
}));
|
|
309
|
+
assertProvided(container, providers, runs.classes, '@MeoCord({ providers })');
|
|
310
|
+
// The store first, after only what it injects: it is ready before anything a call reaches, and shuts down last
|
|
311
|
+
const store = options.cooldownStore;
|
|
312
|
+
const lifecycle = (store ? resolutionOrder(container, providers, [
|
|
313
|
+
store,
|
|
314
|
+
...order
|
|
315
|
+
]) : order).map((token)=>({
|
|
316
|
+
token,
|
|
317
|
+
name: tokenName(token),
|
|
318
|
+
dependencies: tokenDependencies(container, providers, token),
|
|
319
|
+
...token === store && {
|
|
320
|
+
cooldownStore: true
|
|
321
|
+
}
|
|
322
|
+
}));
|
|
323
|
+
// What ShardContext.call reaches: the app's classes, and the classes its providers stand in for
|
|
324
|
+
const callable = [
|
|
325
|
+
...new Set([
|
|
326
|
+
...appClasses,
|
|
327
|
+
...[
|
|
328
|
+
...providers.keys()
|
|
329
|
+
].filter(isAppClassToken)
|
|
330
|
+
])
|
|
331
|
+
];
|
|
332
|
+
// From another shard a call names its class, so with process sharding every name it can reach is one class's
|
|
333
|
+
if (meocordConfig.sharding?.mode === 'process') {
|
|
334
|
+
const names = new Set();
|
|
335
|
+
for (const cls of callable){
|
|
336
|
+
if (names.has(cls.name)) {
|
|
337
|
+
startupError(new Error(`${cls.name}: two classes have this name; with process sharding, ShardContext.call finds a class in another ` + 'shard by its name, so give each controller, service and provided class a distinct name.'));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
names.add(cls.name);
|
|
341
|
+
}
|
|
336
342
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
+
prepareHandlerStages(container, appClasses);
|
|
344
|
+
bindObservers(container, observers);
|
|
345
|
+
// Read here with the rest, so a pattern that cannot be read is reported with them, before the dispatcher warns
|
|
346
|
+
buildMessageRoutes(options.controllers, options.messages);
|
|
347
|
+
// Every check has run, and touched only the container: from here the app is made, once none found an error
|
|
348
|
+
stopOnStartupErrors();
|
|
349
|
+
const discordClient = container.get(Client);
|
|
350
|
+
Reflect.set(discordClient, SHARD_CALL_KEY, shardCallHandler(container, ()=>callable, 'this app'));
|
|
351
|
+
// Stamp every class the app runs with the container, so @UseGuard resolves guards on a direct call to any of them
|
|
352
|
+
for (const cls of runs.classes){
|
|
353
|
+
Reflect.defineMetadata(META.container, container, cls);
|
|
354
|
+
}
|
|
355
|
+
prepareThemes(container, appClasses);
|
|
356
|
+
// Run by start() before it logs in: what may inject a provided value is resolved once every factory
|
|
357
|
+
// has made its value, including those that return a promise
|
|
358
|
+
const logger = this.logger;
|
|
359
|
+
const startup = async ()=>{
|
|
360
|
+
try {
|
|
361
|
+
await resolveProviders(container, providers, order);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
logger.error(`${error.message}. The bot cannot start without it.`);
|
|
364
|
+
logger.debug('Provider failure:', error.cause);
|
|
365
|
+
markExplained(error);
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
// The listed services are made now, so constructors that attach listeners or connect run before login
|
|
369
|
+
for (const svc of options.services ?? []){
|
|
370
|
+
container.get(svc);
|
|
371
|
+
}
|
|
372
|
+
if (shardingRole(meocordConfig) === 'shard' && container.get(CooldownStore) instanceof MemoryCooldownStore) {
|
|
373
|
+
warnPerShardCooldowns(options.controllers, logger);
|
|
374
|
+
}
|
|
375
|
+
bindAppPresenter(container, target, discordClient);
|
|
376
|
+
registerClientTheme(discordClient, container);
|
|
377
|
+
registerClientTranslator(discordClient, options.i18n);
|
|
378
|
+
};
|
|
379
|
+
return new MeoCordApp(options.controllers, container, discordClient, meocordConfig.discordToken, options.activities, appClasses, meocordConfig.shutdownTimeout, startup, lifecycle, options.messages, // In development only, unless the app says otherwise
|
|
380
|
+
options.warnUnanswered ?? process.env.NODE_ENV === 'development');
|
|
381
|
+
});
|
|
343
382
|
}
|
|
344
383
|
}
|
|
345
384
|
MeoCordFactory.logger = new Logger();
|
|
@@ -2,7 +2,7 @@ import { getMessageHandlers } from '../decorator/controller.decorator.js';
|
|
|
2
2
|
import { routeSpecificity } from './route-specificity.js';
|
|
3
3
|
import { isKnownParamType, BUILT_IN_TYPES, isGuildType, fitsParamType } from './message-params.js';
|
|
4
4
|
import { isSpace, splitFlagWords, splitWords, restFrom } from './message-words.js';
|
|
5
|
-
import {
|
|
5
|
+
import { startupError } from '../util/refusal.util.js';
|
|
6
6
|
|
|
7
7
|
/** Whether a route's scope lets it run where the message was sent. */ const fitsScope = (scope, inGuild)=>inGuild === undefined || scope === 'any' || scope === 'guild' === inGuild;
|
|
8
8
|
const PARAM = /^\{(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\.\.\.)?(\?)?\}$/;
|
|
@@ -201,7 +201,8 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
201
201
|
if (own.hidden !== undefined && typeof own.hidden !== 'boolean') throw new Error(`hidden is true or false, not ${JSON.stringify(own.hidden)}.`);
|
|
202
202
|
aliases = aliasPatterns(pattern, parsed.tokens, own.aliases);
|
|
203
203
|
} catch (error) {
|
|
204
|
-
|
|
204
|
+
startupError(new Error(`${controllerClass.name}.${method}: @MessageHandler('${pattern}'): ${error.message}`));
|
|
205
|
+
continue;
|
|
205
206
|
}
|
|
206
207
|
const shared = {
|
|
207
208
|
controllerClass,
|
|
@@ -240,7 +241,7 @@ const FLAG = /^\{--(\w+)(?::([\w-]+(?:\|[\w-]+)*))?(\?)?\}$/;
|
|
|
240
241
|
routes.splice(j--, 1);
|
|
241
242
|
continue;
|
|
242
243
|
}
|
|
243
|
-
|
|
244
|
+
startupError(new Error(`${a.controllerClass.name}.${a.method}: ${describeRoute(a)} and ${describeRoute(b)} in ${b.controllerClass.name}.${b.method} ` + `match the same messages, so only one of them could ever answer those. Change one pattern, or give one its own prefix.`));
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
return routes;
|
|
@@ -4,16 +4,17 @@ import { Logger } from '../common/logger.js';
|
|
|
4
4
|
import { bindShared } from './interceptor-runner.js';
|
|
5
5
|
import { appliesTo } from './stage-scope.js';
|
|
6
6
|
import { respond } from '../common/response/response-state.js';
|
|
7
|
-
import {
|
|
7
|
+
import { startupError } from '../util/refusal.util.js';
|
|
8
8
|
import { META } from '../util/metadata-keys.js';
|
|
9
9
|
|
|
10
10
|
/** Where a container keeps the observers its calls are reported to, in order. */ const OBSERVERS = Symbol('observers');
|
|
11
11
|
const logger = new Logger('Observer');
|
|
12
|
-
/**
|
|
12
|
+
/** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
|
|
13
13
|
for (const observer of observers){
|
|
14
14
|
if (typeof observer !== 'function' || !Reflect.getMetadata(META.observerClass, observer)) {
|
|
15
15
|
const name = typeof observer === 'function' ? observer.name : String(observer);
|
|
16
|
-
|
|
16
|
+
startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
|
|
17
|
+
continue;
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -3,7 +3,7 @@ import { injectedTokens, undecoratedConstructor, untypedParameter } from './guar
|
|
|
3
3
|
import { meocordClasses, meocordClassAdvice } from './meocord-classes.js';
|
|
4
4
|
import { isAppClassToken } from './lifecycle-order.js';
|
|
5
5
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
6
|
-
import { refuse } from '../util/refusal.util.js';
|
|
6
|
+
import { startupError, refuse } from '../util/refusal.util.js';
|
|
7
7
|
|
|
8
8
|
const isValueProvider = (provider)=>'useValue' in provider;
|
|
9
9
|
const isClassProvider = (provider)=>'useClass' in provider;
|
|
@@ -15,9 +15,9 @@ const isFactoryProvider = (provider)=>'useFactory' in provider;
|
|
|
15
15
|
}
|
|
16
16
|
const isToken = (value)=>typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
|
|
17
17
|
/**
|
|
18
|
-
* Checks each provider's shape and indexes them by token,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Checks each provider's shape and indexes them by token, reporting each that cannot be bound, such as one with no
|
|
19
|
+
* token, not exactly one of `useValue`, `useClass` and `useFactory`, one that injects `ExecutionContext`, or a token
|
|
20
|
+
* provided twice, as a startup error; see `startupError`.
|
|
21
21
|
*/ function providerMap(providers, where) {
|
|
22
22
|
const map = new Map();
|
|
23
23
|
for (const provider of providers){
|
|
@@ -26,10 +26,12 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
26
26
|
if (typeof entry === 'function') {
|
|
27
27
|
const name = tokenName(entry);
|
|
28
28
|
const listing = where.includes('@MeoCord') ? 'list a service in @MeoCord({ services })' : 'a class a controller or service injects is bound for you, so it needs no listing';
|
|
29
|
-
|
|
29
|
+
startupError(new Error(`${where}: ${name} is a class, not a provider: ${listing}. To put something in its place, write ` + `{ provide: ${name}, useValue } or { provide: ${name}, useClass }.`));
|
|
30
|
+
continue;
|
|
30
31
|
}
|
|
31
32
|
if (!entry || typeof entry !== 'object' || !isToken(entry.provide)) {
|
|
32
|
-
|
|
33
|
+
startupError(new Error(`${where}: a provider has no token: set provide to a class, a string or a symbol.`));
|
|
34
|
+
continue;
|
|
33
35
|
}
|
|
34
36
|
const name = tokenName(entry.provide);
|
|
35
37
|
const kinds = [
|
|
@@ -38,26 +40,35 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
38
40
|
'useFactory'
|
|
39
41
|
].filter((kind)=>kind in entry);
|
|
40
42
|
if (kinds.length !== 1) {
|
|
41
|
-
|
|
43
|
+
startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
|
|
44
|
+
continue;
|
|
42
45
|
}
|
|
43
46
|
if ('useClass' in entry && typeof entry.useClass !== 'function') {
|
|
44
|
-
|
|
47
|
+
startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
|
|
48
|
+
continue;
|
|
45
49
|
}
|
|
46
50
|
if ('useClass' in entry && injectedTokens(entry.useClass).includes(ExecutionContext)) {
|
|
47
|
-
|
|
51
|
+
startupError(new Error(`${where}: the provider for ${name} uses ${tokenName(entry.useClass)}, which injects ExecutionContext, but it is ` + "made once and shared, so it would keep the first call's context for every later call. Inject " + 'ExecutionContext only into guards.'));
|
|
52
|
+
continue;
|
|
48
53
|
}
|
|
49
54
|
if ('useFactory' in entry) {
|
|
50
55
|
if (typeof entry.useFactory !== 'function') {
|
|
51
|
-
|
|
56
|
+
startupError(new Error(`${where}: the provider for ${name} has a useFactory that is not a function.`));
|
|
57
|
+
continue;
|
|
52
58
|
}
|
|
53
59
|
if (entry.inject !== undefined && !(Array.isArray(entry.inject) && entry.inject.every(isToken))) {
|
|
54
|
-
|
|
60
|
+
startupError(new Error(`${where}: the provider for ${name} has an inject that is not a list of tokens.`));
|
|
61
|
+
continue;
|
|
55
62
|
}
|
|
56
63
|
if (entry.inject?.includes(ExecutionContext)) {
|
|
57
|
-
|
|
64
|
+
startupError(new Error(`${where}: the provider for ${name} injects ExecutionContext, but its factory runs once and its value is ` + "shared, so it would keep the first call's context for every later call. Inject ExecutionContext only into guards."));
|
|
65
|
+
continue;
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
|
-
if (map.has(entry.provide))
|
|
68
|
+
if (map.has(entry.provide)) {
|
|
69
|
+
startupError(new Error(`${where}: ${name} is provided twice.`));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
61
72
|
map.set(entry.provide, provider);
|
|
62
73
|
}
|
|
63
74
|
return map;
|
|
@@ -110,22 +121,23 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
110
121
|
for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
|
|
111
122
|
}
|
|
112
123
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* never missing.
|
|
124
|
+
* Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
|
|
125
|
+
* when the class is first resolved. A class token is bound on demand, so it is never missing.
|
|
116
126
|
*/ function assertProvided(container, providers, classes, where) {
|
|
117
127
|
const missing = (token)=>(typeof token === 'string' || typeof token === 'symbol') && !container.isBound(token);
|
|
118
128
|
for (const cls of classes){
|
|
119
129
|
const token = injectedTokens(cls).find(missing);
|
|
120
130
|
if (token !== undefined) {
|
|
121
|
-
|
|
131
|
+
startupError(new Error(`${cls.name}: it injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
|
|
132
|
+
continue;
|
|
122
133
|
}
|
|
123
134
|
}
|
|
124
135
|
for (const [provided, provider] of providers){
|
|
125
136
|
const dependencies = isFactoryProvider(provider) ? provider.inject ?? [] : isClassProvider(provider) ? injectedTokens(provider.useClass) : [];
|
|
126
137
|
const token = dependencies.find(missing);
|
|
127
138
|
if (token !== undefined) {
|
|
128
|
-
|
|
139
|
+
startupError(new Error(`${tokenName(provided)}: its provider injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
|
|
140
|
+
continue;
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
}
|
|
@@ -158,16 +170,21 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
158
170
|
*/ function assertTypedParameters(classes, decorators) {
|
|
159
171
|
for (const cls of classes){
|
|
160
172
|
const instead = meocordClassAdvice(cls);
|
|
161
|
-
if (instead && undecoratedConstructor(cls))
|
|
173
|
+
if (instead && undecoratedConstructor(cls)) {
|
|
174
|
+
startupError(new Error(`${cls.name}: MeoCord does not inject it; ${instead}.`));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
162
177
|
if (undecoratedConstructor(cls)) {
|
|
163
178
|
const decorator = decorators.get(cls);
|
|
164
|
-
|
|
179
|
+
startupError(new Error(`${cls.name}: its constructor takes parameters, but ${cls.name} has no decorator, so TypeScript recorded none of ` + `their types and it cannot be created. ${decorator ? `Decorate it with ${decorator}.` : 'Decorate it with @Service(), or give a class from a package a provider in @MeoCord({ providers }).'}`));
|
|
180
|
+
continue;
|
|
165
181
|
}
|
|
166
182
|
const index = untypedParameter(cls);
|
|
167
183
|
if (index === -1) continue;
|
|
168
184
|
const injectors = classes.filter((other)=>other !== cls && injectedTokens(other).includes(cls)).map((other)=>other.name);
|
|
169
185
|
const injectedBy = injectors.length === 0 ? '' : ` (${injectors.length === 1 ? injectors[0] : `${injectors.slice(0, -1).join(', ')} and ${injectors.at(-1)}`} ` + `inject${injectors.length === 1 ? 's' : ''} ${cls.name})`;
|
|
170
|
-
|
|
186
|
+
startupError(new Error(`${cls.name}: parameter ${index + 1} of its constructor has no runtime type, so it cannot be created. Usually ` + `${cls.name} and a class it injects import each other${injectedBy}, or the parameter is typed with an ` + 'interface or an `import type`. Move what they both need into a third service, or inject the parameter ' + 'with @Inject(token).'));
|
|
187
|
+
continue;
|
|
171
188
|
}
|
|
172
189
|
}
|
|
173
190
|
/** For each container, the tokens MeoCord binds itself, each to the app's class it stands for, if any. */ const ownTokens = new WeakMap();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* How specific a route pattern is, for ranking the patterns that can match one
|
|
3
|
-
*
|
|
4
|
-
* fewer params, then narrower typed params, whose
|
|
5
|
-
* characters
|
|
2
|
+
* How specific a route pattern is, for ranking the message patterns that can match one message: more literal
|
|
3
|
+
* words first, then a fixed length before one taking the rest, then no optional param before one, then
|
|
4
|
+
* fewer params, then narrower typed params, whose words take fewer values. Component patterns count literal
|
|
5
|
+
* characters, for CommandMeta.specificity alone.
|
|
6
6
|
*/ function routeSpecificity({ literals, params, rest = false, optional = false, typed = 0 }) {
|
|
7
7
|
return literals * 1000 - (rest ? 500 : 0) - (optional ? 250 : 0) - params + typed / 1000;
|
|
8
8
|
}
|