meocord 4.2.0 → 4.2.2
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 +322 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +188 -169
- package/dist/cjs/decorator/index.cjs +52 -7
- package/dist/cjs/testing/index.cjs +1854 -311
- package/dist/cli.json +1 -1
- 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 +13 -3
- 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/errors.js +25 -0
- package/dist/esm/common/meocord-text.js +11 -8
- 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 +89 -15
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +52 -11
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +9 -8
- 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 +10 -4
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +59 -34
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-help.js +32 -6
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/providers.js +19 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/shard-context.js +5 -6
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +7 -2
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +31 -0
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +20 -13
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +225 -30
- package/dist/esm/testing/meocord-testing-module.js +158 -44
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1250 -182
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +40 -17
- 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 +2 -2
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +3 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- 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 +30 -2
- package/dist/types/common/index.d.cts +10 -7
- package/dist/types/common/index.d.ts +10 -7
- package/dist/types/core/index.d.cts +3 -2
- package/dist/types/core/index.d.ts +3 -2
- package/dist/types/decorator/index.d.cts +1 -0
- package/dist/types/decorator/index.d.ts +1 -0
- package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
- package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
- package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
- package/dist/types/testing/index.d.cts +198 -62
- package/dist/types/testing/index.d.ts +198 -62
- package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +2 -2
|
@@ -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;
|
|
@@ -249,22 +249,48 @@ const sortedEntries = (entries)=>entries.sort((a, b)=>a.usage < b.usage ? -1 : a
|
|
|
249
249
|
scope
|
|
250
250
|
].filter((line)=>line !== undefined).join('\n');
|
|
251
251
|
}
|
|
252
|
-
|
|
252
|
+
// Built on first use, for a line longer than a message
|
|
253
|
+
let graphemes;
|
|
254
|
+
/**
|
|
255
|
+
* Where a line longer than one message is cut: after the last character a reader sees as one, such as an emoji, a flag
|
|
256
|
+
* or a letter with its accent, that still fits. A single such character longer than a message is cut between code points.
|
|
257
|
+
*/ function fittingCut(piece) {
|
|
258
|
+
let cut = 0;
|
|
259
|
+
graphemes ?? (graphemes = new Intl.Segmenter(undefined, {
|
|
260
|
+
granularity: 'grapheme'
|
|
261
|
+
}));
|
|
262
|
+
for (const { index, segment } of graphemes.segment(piece)){
|
|
263
|
+
if (index + segment.length > MAX_REPLY_LENGTH) break;
|
|
264
|
+
cut = index + segment.length;
|
|
265
|
+
}
|
|
266
|
+
if (cut > 0) return cut;
|
|
267
|
+
const high = piece.charCodeAt(MAX_REPLY_LENGTH - 1);
|
|
268
|
+
return high >= 0xd800 && high <= 0xdbff ? MAX_REPLY_LENGTH - 1 : MAX_REPLY_LENGTH;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. A
|
|
272
|
+
* message of only whitespace, which Discord refuses as empty, is left out.
|
|
273
|
+
*/ function splitReply(text) {
|
|
253
274
|
const chunks = [];
|
|
275
|
+
const push = (chunk)=>{
|
|
276
|
+
if (chunk.trim() !== '') chunks.push(chunk);
|
|
277
|
+
};
|
|
254
278
|
let current = '';
|
|
255
279
|
for (const line of text.split('\n')){
|
|
256
|
-
for(let piece = line;;
|
|
257
|
-
const
|
|
280
|
+
for(let piece = line;;){
|
|
281
|
+
const cut = piece.length <= MAX_REPLY_LENGTH ? piece.length : fittingCut(piece);
|
|
282
|
+
const part = piece.slice(0, cut);
|
|
258
283
|
const joined = current ? `${current}\n${part}` : part;
|
|
259
284
|
if (joined.length <= MAX_REPLY_LENGTH) current = joined;
|
|
260
285
|
else {
|
|
261
|
-
|
|
286
|
+
push(current);
|
|
262
287
|
current = part;
|
|
263
288
|
}
|
|
264
|
-
if (piece.length
|
|
289
|
+
if (cut === piece.length) break;
|
|
290
|
+
piece = piece.slice(cut);
|
|
265
291
|
}
|
|
266
292
|
}
|
|
267
|
-
|
|
293
|
+
push(current);
|
|
268
294
|
return chunks;
|
|
269
295
|
}
|
|
270
296
|
|
|
@@ -5,6 +5,7 @@ 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
7
|
import { startupError } from '../util/refusal.util.js';
|
|
8
|
+
import { isConstructor } from '../util/value.util.js';
|
|
8
9
|
import { META } from '../util/metadata-keys.js';
|
|
9
10
|
|
|
10
11
|
/** Where a container keeps the observers its calls are reported to, in order. */ const OBSERVERS = Symbol('observers');
|
|
@@ -12,7 +13,7 @@ const logger = new Logger('Observer');
|
|
|
12
13
|
/** Reports each entry that is not an `@Observer` class, naming where it was listed. */ function assertObservers(where, observers) {
|
|
13
14
|
for (const observer of observers){
|
|
14
15
|
if (typeof observer !== 'function' || !Reflect.getMetadata(META.observerClass, observer)) {
|
|
15
|
-
const name = typeof observer === 'function' ?
|
|
16
|
+
const name = isConstructor(observer) ? observer.name || 'an anonymous class' : typeof observer === 'function' ? 'a function' : String(observer);
|
|
16
17
|
startupError(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
|
|
17
18
|
continue;
|
|
18
19
|
}
|
|
@@ -4,6 +4,7 @@ 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
6
|
import { startupError, refuse } from '../util/refusal.util.js';
|
|
7
|
+
import { isConstructor } from '../util/value.util.js';
|
|
7
8
|
|
|
8
9
|
const isValueProvider = (provider)=>'useValue' in provider;
|
|
9
10
|
const isClassProvider = (provider)=>'useClass' in provider;
|
|
@@ -43,7 +44,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
43
44
|
startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
|
|
44
45
|
continue;
|
|
45
46
|
}
|
|
46
|
-
if ('useClass' in entry &&
|
|
47
|
+
if ('useClass' in entry && !isConstructor(entry.useClass)) {
|
|
47
48
|
startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
@@ -120,6 +121,15 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
120
121
|
});
|
|
121
122
|
for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Reports each service listed by a string or symbol token that no provider provides, which would otherwise be bound
|
|
126
|
+
* as if it were a class.
|
|
127
|
+
*/ function assertProvidedServices(appName, services, providers, where) {
|
|
128
|
+
for (const service of services){
|
|
129
|
+
if (typeof service === 'function' || providers.has(service)) continue;
|
|
130
|
+
startupError(new Error(`${appName}: @MeoCord({ services }) lists ${tokenName(service)}, which nothing provides: add a provider for it to ${where}.`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
123
133
|
/**
|
|
124
134
|
* Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
|
|
125
135
|
* when the class is first resolved. A class token is bound on demand, so it is never missing.
|
|
@@ -253,17 +263,22 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
253
263
|
}
|
|
254
264
|
/**
|
|
255
265
|
* Runs every factory in `order`, awaiting those that return a promise, so each value is made before
|
|
256
|
-
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails.
|
|
257
|
-
|
|
266
|
+
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails. With `control`,
|
|
267
|
+
* it makes no further value once the app is stopping.
|
|
268
|
+
*/ async function resolveProviders(container, providers, order, control) {
|
|
258
269
|
for (const token of order){
|
|
259
270
|
const provider = providers.get(token);
|
|
260
271
|
if (!provider || !isFactoryProvider(provider)) continue;
|
|
272
|
+
if (control?.stopped()) return;
|
|
273
|
+
control?.pending(tokenName(token));
|
|
261
274
|
try {
|
|
262
275
|
await container.getAsync(token);
|
|
263
276
|
} catch (error) {
|
|
264
277
|
throw providerFailure(token, error);
|
|
278
|
+
} finally{
|
|
279
|
+
control?.pending(undefined);
|
|
265
280
|
}
|
|
266
281
|
}
|
|
267
282
|
}
|
|
268
283
|
|
|
269
|
-
export { assertProvided, assertTypedParameters, bindProvider, bindsOwnToken, isClassProvider, isFactoryProvider, isValueProvider, providerFailure, providerMap, reachableClasses, resolutionOrder, resolveProviders, tokenDependencies, tokenName };
|
|
284
|
+
export { assertProvided, assertProvidedServices, assertTypedParameters, bindProvider, bindsOwnToken, isClassProvider, isFactoryProvider, isValueProvider, providerFailure, providerMap, reachableClasses, resolutionOrder, resolveProviders, tokenDependencies, tokenName };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The calls an app is running, which shutdown waits for before the `onShutdown` hooks. A call that asks the app to
|
|
5
|
+
* stop is excused, so a handler awaiting `app.stop()` never waits for itself.
|
|
6
|
+
*/ class RunningCalls {
|
|
7
|
+
/** Runs `run` as a call named `label`, tracked until it settles. */ run(label, run) {
|
|
8
|
+
const token = {};
|
|
9
|
+
this.calls.set(token, label);
|
|
10
|
+
const done = this.current.run(token, run);
|
|
11
|
+
const settle = ()=>{
|
|
12
|
+
this.calls.delete(token);
|
|
13
|
+
this.notify();
|
|
14
|
+
};
|
|
15
|
+
done.then(settle, settle);
|
|
16
|
+
return done;
|
|
17
|
+
}
|
|
18
|
+
/** Excuses the call this runs in, and any it runs within, from the wait; outside a call, nothing. */ excuseCurrent() {
|
|
19
|
+
const token = this.current.getStore();
|
|
20
|
+
if (!token || !this.calls.has(token)) return;
|
|
21
|
+
this.excused.add(token);
|
|
22
|
+
this.notify();
|
|
23
|
+
}
|
|
24
|
+
/** The names of the calls a wait would still wait for. */ running() {
|
|
25
|
+
return [
|
|
26
|
+
...this.calls
|
|
27
|
+
].filter(([token])=>!this.excused.has(token)).map(([, label])=>label);
|
|
28
|
+
}
|
|
29
|
+
/** Settles once every call not excused has, those that start or are excused meanwhile included. */ async settled() {
|
|
30
|
+
while(this.running().length > 0)await (this.changed ?? (this.changed = Promise.withResolvers())).promise;
|
|
31
|
+
}
|
|
32
|
+
notify() {
|
|
33
|
+
this.changed?.resolve();
|
|
34
|
+
this.changed = undefined;
|
|
35
|
+
}
|
|
36
|
+
constructor(){
|
|
37
|
+
this.current = new AsyncLocalStorage();
|
|
38
|
+
/** Each call under way, by its token, with the name a log gives it. */ this.calls = new Map();
|
|
39
|
+
this.excused = new WeakSet();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { RunningCalls };
|
|
@@ -5,16 +5,15 @@ import './theme-validation.js';
|
|
|
5
5
|
import '../common/decorator.js';
|
|
6
6
|
import 'reflect-metadata';
|
|
7
7
|
import 'discord.js';
|
|
8
|
-
import '../common/
|
|
9
|
-
import '../common/
|
|
10
|
-
import '../util/interaction.util.js';
|
|
11
|
-
import '../common/response/flags.js';
|
|
12
|
-
import 'path';
|
|
13
|
-
import '../common/response/components.js';
|
|
8
|
+
import '../common/response/response-state.js';
|
|
9
|
+
import '../common/errors.js';
|
|
14
10
|
import '../decorator/controller.decorator.js';
|
|
15
11
|
import './scalar-types.js';
|
|
12
|
+
import 'path';
|
|
16
13
|
import 'node:fs';
|
|
17
14
|
import 'node:url';
|
|
15
|
+
import '../common/translator.js';
|
|
16
|
+
import '../common/meocord-text.js';
|
|
18
17
|
|
|
19
18
|
/** Where each shard's client keeps the function that runs a `ShardContext.call` in that shard. */ const SHARD_CALL_KEY = Symbol.for('meocord.shardCall');
|
|
20
19
|
const packArgs = (args)=>Object.assign({
|