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
|
@@ -5,18 +5,15 @@ import './theme-scope.js';
|
|
|
5
5
|
import './theme-validation.js';
|
|
6
6
|
import '../common/decorator.js';
|
|
7
7
|
import 'reflect-metadata';
|
|
8
|
+
import '../common/response/response-state.js';
|
|
8
9
|
import { GuardDeniedError, UserError } from '../common/errors.js';
|
|
9
|
-
import '../common/translator.js';
|
|
10
|
-
import '../common/response/send-failure.js';
|
|
11
|
-
import '../util/interaction.util.js';
|
|
12
|
-
import '../common/response/flags.js';
|
|
13
|
-
import 'path';
|
|
14
|
-
import '../common/response/components.js';
|
|
15
10
|
import 'node:crypto';
|
|
16
11
|
import { getMessageHandlers, getReactionHandlers } from '../decorator/controller.decorator.js';
|
|
17
12
|
import './scalar-types.js';
|
|
18
13
|
import { isShardMessage } from './shard-messages.js';
|
|
19
14
|
import { managerGone } from '../util/sharding-mode.util.js';
|
|
15
|
+
import '../common/translator.js';
|
|
16
|
+
import '../common/meocord-text.js';
|
|
20
17
|
import { markExplained } from '../common/explained-error.js';
|
|
21
18
|
import { messageDecorator, reactionDecorator } from './command-conflicts.js';
|
|
22
19
|
import { ReactionHandlerAction } from '../enum/controller.enum.js';
|
|
@@ -27,7 +24,8 @@ import { stageTypes, stageClass } from './stage-scope.js';
|
|
|
27
24
|
import { getEventHandlers } from '../decorator/event.decorator.js';
|
|
28
25
|
import { eventRequirements, messageHandlerRequirements, REACTION_HANDLER_REQUIREMENTS, missingRequirementWarnings } from './event-requirements.js';
|
|
29
26
|
import { classUnits } from './lifecycle-order.js';
|
|
30
|
-
import { runReadyHooks, runShutdownSequence,
|
|
27
|
+
import { runReadyHooks, runShutdownSequence, ReadyPass } from './lifecycle-hooks.js';
|
|
28
|
+
import { RunningCalls } from './running-calls.js';
|
|
31
29
|
import { stopRequests } from '../util/stop-request.util.js';
|
|
32
30
|
import { tellDevRunner, onDevRunnerStop } from '../util/dev-runner.util.js';
|
|
33
31
|
import { fatalLoginCode, explainLoginFailure, tokenMessage, isRefusedToken } from './login-failure.js';
|
|
@@ -41,7 +39,6 @@ import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js';
|
|
|
41
39
|
import { isRegisterOnly, FORCE_REGISTER_ENV, REGISTER_GUILD_ENV } from '../util/registration-mode.util.js';
|
|
42
40
|
import { shutdownTimeoutOf } from '../util/shutdown-timeout.util.js';
|
|
43
41
|
import { escapeForLog } from '../util/user-text.util.js';
|
|
44
|
-
import { isShardProcess } from '../util/shard-process.util.js';
|
|
45
42
|
|
|
46
43
|
/** How long an `onReady` hook runs before a warning says the hooks after it are waiting. */ const SLOW_READY_HOOK_MS = 10000;
|
|
47
44
|
/** Closes each started app: runs its shutdown hooks and destroys its client, resolving `false` on failure. */ const runningApps = new Set();
|
|
@@ -52,11 +49,11 @@ let shuttingDown;
|
|
|
52
49
|
* Shuts every started app down and exits, with a code already set or 0, and 1 if a client failed to close. Signals call
|
|
53
50
|
* it, and in a shard its manager's request or its going. A call within `REPEAT_SIGNAL_WINDOW_MS` of the first is the
|
|
54
51
|
* same request; one after it forces exit 1, except in a shard, whose manager forces it.
|
|
55
|
-
*/ async function shutdownAndExit() {
|
|
52
|
+
*/ async function shutdownAndExit(shard) {
|
|
56
53
|
const request = stopRequest();
|
|
57
54
|
if (request !== 'first') {
|
|
58
55
|
// A shard hears Ctrl+C both directly and from its manager, which owns forcing it; so it waits
|
|
59
|
-
if (request === 'duplicate' ||
|
|
56
|
+
if (request === 'duplicate' || shard) return;
|
|
60
57
|
process.exit(1);
|
|
61
58
|
return;
|
|
62
59
|
}
|
|
@@ -77,47 +74,48 @@ let shuttingDown;
|
|
|
77
74
|
markExplained(error);
|
|
78
75
|
return error;
|
|
79
76
|
}
|
|
80
|
-
/** One pair of signal listeners for the process, however many apps it starts. */ function installSignalHandlers() {
|
|
77
|
+
/** One pair of signal listeners for the process, however many apps it starts, for the role the first decided. */ function installSignalHandlers(shard) {
|
|
81
78
|
if (signalHandlersInstalled) return;
|
|
82
79
|
signalHandlersInstalled = true;
|
|
83
|
-
process.on('SIGINT', ()=>void shutdownAndExit());
|
|
84
|
-
process.on('SIGTERM', ()=>void shutdownAndExit());
|
|
80
|
+
process.on('SIGINT', ()=>void shutdownAndExit(shard));
|
|
81
|
+
process.on('SIGTERM', ()=>void shutdownAndExit(shard));
|
|
85
82
|
// How `meocord start --dev` stops the application to restart it, a signal being no graceful stop on Windows. It is
|
|
86
83
|
// not the user's stop request: their first Ctrl+C during it joins this shutdown, and only a repeat forces exit 1.
|
|
87
|
-
onDevRunnerStop(()=>void shutDown());
|
|
84
|
+
onDevRunnerStop(()=>void shutDown(), shard);
|
|
88
85
|
// A shard stops when its manager asks, or when the manager is gone and cannot ask
|
|
89
|
-
if (
|
|
86
|
+
if (shard) {
|
|
90
87
|
process.on('message', (message)=>{
|
|
91
|
-
if (isShardMessage(message) && message.meocord === 'shutdown') void shutdownAndExit();
|
|
88
|
+
if (isShardMessage(message) && message.meocord === 'shutdown') void shutdownAndExit(shard);
|
|
92
89
|
});
|
|
93
|
-
process.on('disconnect', ()=>void shutdownAndExit());
|
|
90
|
+
process.on('disconnect', ()=>void shutdownAndExit(shard));
|
|
94
91
|
}
|
|
95
92
|
}
|
|
96
|
-
/** Tells the manager a shard cannot log in, and why, and waits until the message is sent. */ async function reportFatalLogin(code, reason) {
|
|
97
|
-
if (
|
|
93
|
+
/** Tells the manager a shard cannot log in, and why, and waits until the message is sent. */ async function reportFatalLogin(shard, code, reason) {
|
|
94
|
+
if (shard) await tellManager({
|
|
98
95
|
meocord: 'fatal',
|
|
99
96
|
code,
|
|
100
97
|
message: reason
|
|
101
98
|
});
|
|
102
99
|
}
|
|
100
|
+
/** How a shutdown warning names an interaction still being handled: by its command, or a component's customId. */ function interactionLabel(interaction) {
|
|
101
|
+
if ('commandName' in interaction) return `interaction "/${escapeForLog(interaction.commandName)}"`;
|
|
102
|
+
if ('customId' in interaction) return `interaction "${escapeForLog(interaction.customId)}"`;
|
|
103
|
+
return '"interactionCreate"';
|
|
104
|
+
}
|
|
103
105
|
class MeoCordApp {
|
|
104
106
|
/**
|
|
105
107
|
* Runs an event handler so its failure is logged against the event instead of surfacing as an
|
|
106
108
|
* unhandled rejection, which would terminate the whole bot.
|
|
107
|
-
*/ async runListener(event, run) {
|
|
108
|
-
const
|
|
109
|
+
*/ async runListener(event, run, label = `"${event}"`) {
|
|
110
|
+
const handle = async ()=>{
|
|
109
111
|
try {
|
|
110
112
|
await run();
|
|
111
113
|
} catch (error) {
|
|
112
114
|
this.logger.error(`Unhandled error while handling "${event}":`, error);
|
|
113
115
|
}
|
|
114
|
-
}
|
|
115
|
-
// A call, which
|
|
116
|
-
|
|
117
|
-
this.calls.add(handled);
|
|
118
|
-
void handled.then(()=>this.calls.delete(handled));
|
|
119
|
-
}
|
|
120
|
-
await handled;
|
|
116
|
+
};
|
|
117
|
+
// A call, which shutdown waits for before the onShutdown hooks; the ready listener's work is not one
|
|
118
|
+
await (event === 'clientReady' ? handle() : this.calls.run(label, handle));
|
|
121
119
|
}
|
|
122
120
|
/**
|
|
123
121
|
* Shows the next of the app's activities, in the order listed, starting again after the last. Guarded apart from
|
|
@@ -139,7 +137,7 @@ class MeoCordApp {
|
|
|
139
137
|
if (this.online) return;
|
|
140
138
|
this.starting ?? (this.starting = this.startOnce().catch((error)=>{
|
|
141
139
|
// A shard ends, for its manager to restart, or to stop all if told it is fatal; a stopped start is the stop's
|
|
142
|
-
if (
|
|
140
|
+
if (this.shard && !this.closing) endFailedShard(error);
|
|
143
141
|
throw error;
|
|
144
142
|
}).finally(()=>this.starting = undefined));
|
|
145
143
|
return this.starting;
|
|
@@ -149,7 +147,10 @@ class MeoCordApp {
|
|
|
149
147
|
* which then asks this one too, so the process ends. A client that fails to close sets `process.exitCode` to 1,
|
|
150
148
|
* unless a code is already set.
|
|
151
149
|
*/ async stop() {
|
|
152
|
-
|
|
150
|
+
// A handler or an onReady awaiting stop() would otherwise hold up the shutdown that waits for it
|
|
151
|
+
this.calls.excuseCurrent();
|
|
152
|
+
this.readyPass.excuseCurrent();
|
|
153
|
+
if (this.shard) await tellManager({
|
|
153
154
|
meocord: 'stop'
|
|
154
155
|
});
|
|
155
156
|
// As a signal's shutdown exits 1 for a client that failed to close, a code another failure set aside
|
|
@@ -159,14 +160,20 @@ class MeoCordApp {
|
|
|
159
160
|
this.logger.log('Starting bot...');
|
|
160
161
|
// Before the providers are made, so a signal, or a shard's manager, can stop a start that is slow to come online
|
|
161
162
|
runningApps.add(this.close);
|
|
162
|
-
installSignalHandlers();
|
|
163
|
+
installSignalHandlers(this.shard);
|
|
163
164
|
// A shard whose manager is already gone has no one to answer to, and nothing would stop it later
|
|
164
|
-
if (
|
|
165
|
+
if (this.shard && managerGone()) void shutdownAndExit(this.shard);
|
|
165
166
|
// A bot runs one app, whose theme code outside any call reads, its providers' factories included; claimed for
|
|
166
167
|
// each start, as a failed one gives it up
|
|
167
168
|
claimAmbientAppTheme(this.container);
|
|
168
169
|
if (!this.prepared) {
|
|
169
|
-
|
|
170
|
+
// Kept while it runs, so a stop meanwhile waits for it
|
|
171
|
+
this.preparing = this.prepare();
|
|
172
|
+
try {
|
|
173
|
+
await this.preparing;
|
|
174
|
+
} finally{
|
|
175
|
+
this.preparing = undefined;
|
|
176
|
+
}
|
|
170
177
|
this.prepared = true;
|
|
171
178
|
}
|
|
172
179
|
if (this.loginFailed) {
|
|
@@ -179,7 +186,10 @@ class MeoCordApp {
|
|
|
179
186
|
// Every provided value is made before anything that injects it is resolved, and before login
|
|
180
187
|
if (this.startup) {
|
|
181
188
|
try {
|
|
182
|
-
await this.startup(
|
|
189
|
+
await this.startup({
|
|
190
|
+
stopped: ()=>this.closing,
|
|
191
|
+
pending: (name)=>this.pendingProvider = name
|
|
192
|
+
});
|
|
183
193
|
} catch (error) {
|
|
184
194
|
releaseAmbientAppTheme(this.container);
|
|
185
195
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
@@ -189,7 +199,8 @@ class MeoCordApp {
|
|
|
189
199
|
throw error;
|
|
190
200
|
}
|
|
191
201
|
}
|
|
192
|
-
// A stop that came while the providers were being made has
|
|
202
|
+
// A stop that came while the providers were being made has waited for them, and the startup made no more after it;
|
|
203
|
+
// nothing is attached or logged in
|
|
193
204
|
if (this.closing) throw stoppedBeforeOnline();
|
|
194
205
|
// Calls wait for the store's onReady, which may connect it, before asking it
|
|
195
206
|
if (this.lifecycleUnits.some((unit)=>unit.cooldownStore)) {
|
|
@@ -205,10 +216,10 @@ class MeoCordApp {
|
|
|
205
216
|
// Started before registration and not waited on by it, so a slow or failed registration never holds them up
|
|
206
217
|
const readyHooks = this.runReadyHooks(readyClient ?? this.bot);
|
|
207
218
|
// With process sharding, the manager registers once for every shard
|
|
208
|
-
if (!
|
|
219
|
+
if (!this.shard) await this.registerCommands();
|
|
209
220
|
await readyHooks;
|
|
210
221
|
}));
|
|
211
|
-
this.bot.on('interactionCreate', ownInteractionListener((interaction)=>this.runListener('interactionCreate', ()=>this.dispatcher.interaction(interaction))));
|
|
222
|
+
this.bot.on('interactionCreate', ownInteractionListener((interaction)=>this.runListener('interactionCreate', ()=>this.dispatcher.interaction(interaction), interactionLabel(interaction))));
|
|
212
223
|
this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.dispatcher.message(message)));
|
|
213
224
|
this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.dispatcher.reaction(reaction, {
|
|
214
225
|
user,
|
|
@@ -251,18 +262,18 @@ class MeoCordApp {
|
|
|
251
262
|
const explanation = fatal && explainLoginFailure(fatal, this.bot.options?.intents, this.discordToken);
|
|
252
263
|
if (explanation) {
|
|
253
264
|
// The explanation is what to act on, and the stack only for debugging. A shard's manager logs it instead.
|
|
254
|
-
if (!
|
|
265
|
+
if (!this.shard) this.logger.error(explanation);
|
|
255
266
|
this.logger.debug('Login failed:', error);
|
|
256
267
|
markExplained(error);
|
|
257
268
|
}
|
|
258
|
-
if (fatal) await reportFatalLogin(fatal, explanation ?? (error instanceof Error ? error.message : String(error)));
|
|
269
|
+
if (fatal) await reportFatalLogin(this.shard, fatal, explanation ?? (error instanceof Error ? error.message : String(error)));
|
|
259
270
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
260
271
|
process.exitCode = 1;
|
|
261
272
|
MeoCordApp.failedLoginSetExitCode = true;
|
|
262
273
|
}
|
|
263
274
|
await tellDevRunner({
|
|
264
275
|
meocord: 'login-failed'
|
|
265
|
-
});
|
|
276
|
+
}, this.shard);
|
|
266
277
|
MeoCordApp.toldDevRunnerLoginFailed = true;
|
|
267
278
|
this.loginFailed = true;
|
|
268
279
|
throw error;
|
|
@@ -280,7 +291,7 @@ class MeoCordApp {
|
|
|
280
291
|
MeoCordApp.toldDevRunnerLoginFailed = false;
|
|
281
292
|
await tellDevRunner({
|
|
282
293
|
meocord: 'online'
|
|
283
|
-
});
|
|
294
|
+
}, this.shard);
|
|
284
295
|
}
|
|
285
296
|
this.online = true;
|
|
286
297
|
this.logger.log('Bot is online!');
|
|
@@ -363,18 +374,20 @@ class MeoCordApp {
|
|
|
363
374
|
} else logError(error);
|
|
364
375
|
return undefined;
|
|
365
376
|
};
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
377
|
+
const label = `@${once ? 'Once' : 'On'}('${event}') in ${lifecycleClass.name}.${method}`;
|
|
378
|
+
// A call, as a dispatched one is, which shutdown waits for
|
|
379
|
+
const listener = (...args)=>this.calls.run(label, async ()=>{
|
|
380
|
+
try {
|
|
381
|
+
const instance = this.container.get(lifecycleClass);
|
|
382
|
+
await runHandler(this.container, instance, method, args, {
|
|
383
|
+
fallback,
|
|
384
|
+
type: 'event'
|
|
385
|
+
});
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// Only resolving the instance can fail here; the pipeline hands every other error to the fallback
|
|
388
|
+
logError(error);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
378
391
|
if (once) this.bot.once(event, listener);
|
|
379
392
|
else this.bot.on(event, listener);
|
|
380
393
|
}
|
|
@@ -443,29 +456,35 @@ class MeoCordApp {
|
|
|
443
456
|
*/ async runReadyHooks(client) {
|
|
444
457
|
const entries = [];
|
|
445
458
|
this.lifecycleEntries = entries;
|
|
446
|
-
await runReadyHooks(this.container, this.lifecycleUnits, client, {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
+
await this.readyPass.run(()=>runReadyHooks(this.container, this.lifecycleUnits, client, {
|
|
460
|
+
primary: client.shard ? client.shard.ids.includes(0) : true
|
|
461
|
+
}, entries, {
|
|
462
|
+
resolveFailed: (unit, error)=>this.logger.error(`Could not resolve ${unit.name} to run its lifecycle hooks:`, error),
|
|
463
|
+
hookFailed: (unit, error)=>this.logger.error(`onReady failed in ${unit.name}:`, error),
|
|
464
|
+
dependsOnFailed: (unit, failed)=>this.logger.warn(`Running onReady in ${unit.name} although it depends on ${failed.map(({ name })=>name).join(', ')}, which failed.`),
|
|
465
|
+
slow: (unit)=>this.logger.warn(`onReady in ${unit.name} has run for over ${SLOW_READY_HOOK_MS} ms; the hooks after it are waiting.`)
|
|
466
|
+
}, // Shutdown has begun: the client is going away, so no further hook starts
|
|
467
|
+
{
|
|
468
|
+
stopped: ()=>this.closing,
|
|
469
|
+
slowAfterMs: SLOW_READY_HOOK_MS,
|
|
470
|
+
settled: (unit)=>unit.cooldownStore && this.storeReady?.resolve(),
|
|
471
|
+
runOnReady: this.readyPass.runOnReady
|
|
472
|
+
}));
|
|
459
473
|
// Also when shutdown stopped the hooks before the store's, so no call waits on it
|
|
460
474
|
this.storeReady?.resolve();
|
|
461
475
|
}
|
|
462
|
-
/** Runs the `onShutdown` hooks through the shutdown sequence, within the configured `shutdownTimeout`. */ async runShutdownHooks(
|
|
463
|
-
|
|
464
|
-
|
|
476
|
+
/** Runs the `onShutdown` hooks through the shutdown sequence, within the configured `shutdownTimeout`. */ async runShutdownHooks() {
|
|
477
|
+
// Read once the onReady in progress settled; a login that failed never ran onReady, so there is no onShutdown to undo
|
|
478
|
+
await runShutdownSequence(this.container, ()=>this.lifecycleEntries ?? [], {
|
|
479
|
+
// No new call starts once the listeners are gone; an error the client emits meanwhile still has its listeners
|
|
465
480
|
drainCalls: ()=>{
|
|
466
|
-
this.bot
|
|
467
|
-
|
|
481
|
+
const emitter = this.bot;
|
|
482
|
+
for (const event of emitter.eventNames())if (event !== 'error') emitter.removeAllListeners(event);
|
|
483
|
+
return this.calls.settled();
|
|
468
484
|
},
|
|
485
|
+
drainAlways: true,
|
|
486
|
+
runningCalls: ()=>this.calls.running(),
|
|
487
|
+
readyPass: this.readyPass,
|
|
469
488
|
timeoutMs: this.shutdownTimeout,
|
|
470
489
|
hookFailed: (name, error)=>this.logger.error(`onShutdown failed in ${name}:`, error),
|
|
471
490
|
warn: (message)=>this.logger.warn(message)
|
|
@@ -484,8 +503,28 @@ class MeoCordApp {
|
|
|
484
503
|
releaseAmbientAppTheme(this.container);
|
|
485
504
|
}
|
|
486
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* Waits for the startup under way to stop making providers and services, at most `shutdownTimeout`; past it, warns
|
|
508
|
+
* with the factory it still awaits, which then makes nothing more.
|
|
509
|
+
*/ async waitForStartup(preparing) {
|
|
510
|
+
let timer;
|
|
511
|
+
const outlasted = new Promise((resolve)=>{
|
|
512
|
+
timer = setTimeout(()=>resolve(true), this.shutdownTimeout);
|
|
513
|
+
});
|
|
514
|
+
const settled = preparing.then(()=>false, ()=>false);
|
|
515
|
+
if (await Promise.race([
|
|
516
|
+
settled,
|
|
517
|
+
outlasted
|
|
518
|
+
])) {
|
|
519
|
+
const what = this.pendingProvider ? `the factory providing ${this.pendingProvider}` : 'its providers';
|
|
520
|
+
this.logger.warn(`The startup still waits for ${what} after ${this.shutdownTimeout} ms; shutting down without it.`);
|
|
521
|
+
}
|
|
522
|
+
clearTimeout(timer);
|
|
523
|
+
}
|
|
487
524
|
async shutDownClient() {
|
|
488
525
|
this.logger.log('Shutting down bot...');
|
|
526
|
+
// So nothing is made after the bot has shut down
|
|
527
|
+
if (this.preparing) await this.waitForStartup(this.preparing);
|
|
489
528
|
// Nothing came online, so there are no hooks to undo; the listeners go first, so none of them runs
|
|
490
529
|
if (this.abortLogin) {
|
|
491
530
|
this.logger.log('The bot was still logging in, so it stops without coming online');
|
|
@@ -495,8 +534,8 @@ class MeoCordApp {
|
|
|
495
534
|
return true;
|
|
496
535
|
}
|
|
497
536
|
if (this.activityInterval) clearInterval(this.activityInterval);
|
|
498
|
-
//
|
|
499
|
-
|
|
537
|
+
// The calls drain on every shutdown, and the onReady in progress settles first
|
|
538
|
+
await this.runShutdownHooks();
|
|
500
539
|
try {
|
|
501
540
|
this.bot.removeAllListeners();
|
|
502
541
|
await this.bot.destroy();
|
|
@@ -507,7 +546,7 @@ class MeoCordApp {
|
|
|
507
546
|
return false;
|
|
508
547
|
}
|
|
509
548
|
}
|
|
510
|
-
constructor(controllerClasses, container, discordClient, discordToken, activities, lifecycleClasses = [], shutdownTimeout, startup, lifecycleUnits, messageOptions = {}, warnUnanswered = false){
|
|
549
|
+
constructor(controllerClasses, container, discordClient, discordToken, activities, lifecycleClasses = [], shutdownTimeout, startup, lifecycleUnits, messageOptions = {}, warnUnanswered = false, /** Whether a manager spawned this process, as `MeoCordFactory.create` decided before anything could change it. */ shard = false){
|
|
511
550
|
this.controllerClasses = controllerClasses;
|
|
512
551
|
this.container = container;
|
|
513
552
|
this.discordClient = discordClient;
|
|
@@ -516,6 +555,7 @@ class MeoCordApp {
|
|
|
516
555
|
this.lifecycleClasses = lifecycleClasses;
|
|
517
556
|
this.startup = startup;
|
|
518
557
|
this.messageOptions = messageOptions;
|
|
558
|
+
this.shard = shard;
|
|
519
559
|
this.logger = new Logger(MeoCordApp.name);
|
|
520
560
|
this.fallback = createFallback(this.logger, ()=>this.messageOptions, {
|
|
521
561
|
cooldownNotice: (refusal)=>claimCooldownNotice(this.container, refusal),
|
|
@@ -528,7 +568,8 @@ class MeoCordApp {
|
|
|
528
568
|
* Closes the app once, however many stops and signals ask: whether the client was destroyed cleanly. The app stays
|
|
529
569
|
* among the running ones until the close settles, so a signal or a manager's request meanwhile waits for it.
|
|
530
570
|
*/ this.close = ()=>this.stopped ?? (this.stopped = this.closeClient().finally(()=>runningApps.delete(this.close)));
|
|
531
|
-
/** The calls under way,
|
|
571
|
+
/** The calls under way, which shutdown waits for, less one that asked the app to stop. */ this.calls = new RunningCalls();
|
|
572
|
+
/** The ready hooks' pass, whose `onReady` in progress shutdown waits for, less one that asked the app to stop. */ this.readyPass = new ReadyPass();
|
|
532
573
|
/** Whether the providers, services and event handlers are set up, which a retry after a failed login reuses. */ this.prepared = false;
|
|
533
574
|
/** Whether the bot logged in, after which `start()` has nothing to do. */ this.online = false;
|
|
534
575
|
/** Whether a login of this app failed, so a retry restores the client discord.js destroyed. */ this.loginFailed = false;
|
|
@@ -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();
|
|
@@ -236,15 +253,20 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
236
253
|
}
|
|
237
254
|
/**
|
|
238
255
|
* Runs every factory in `order`, awaiting those that return a promise, so each value is made before
|
|
239
|
-
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails.
|
|
240
|
-
|
|
256
|
+
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails. With `control`,
|
|
257
|
+
* it makes no further value once the app is stopping.
|
|
258
|
+
*/ async function resolveProviders(container, providers, order, control) {
|
|
241
259
|
for (const token of order){
|
|
242
260
|
const provider = providers.get(token);
|
|
243
261
|
if (!provider || !isFactoryProvider(provider)) continue;
|
|
262
|
+
if (control?.stopped()) return;
|
|
263
|
+
control?.pending(tokenName(token));
|
|
244
264
|
try {
|
|
245
265
|
await container.getAsync(token);
|
|
246
266
|
} catch (error) {
|
|
247
267
|
throw providerFailure(token, error);
|
|
268
|
+
} finally{
|
|
269
|
+
control?.pending(undefined);
|
|
248
270
|
}
|
|
249
271
|
}
|
|
250
272
|
}
|
|
@@ -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
|
}
|