meocord 4.2.0 → 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 +264 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-XKMlcUZz.cjs} +376 -100
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-BZgJyfZS.cjs} +145 -36
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-CgfPQrNl.cjs} +219 -63
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-BUAR1WYG.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +185 -168
- package/dist/cjs/decorator/index.cjs +21 -6
- package/dist/cjs/testing/index.cjs +1594 -286
- 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 +84 -10
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +37 -6
- 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/fallback.js +20 -11
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +55 -32
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/providers.js +7 -2
- 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/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +3 -1
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +26 -10
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +205 -30
- package/dist/esm/testing/meocord-testing-module.js +134 -28
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1026 -172
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +37 -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 +12 -1
- package/dist/types/common/index.d.cts +6 -6
- package/dist/types/common/index.d.ts +6 -6
- 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 +175 -53
- package/dist/types/testing/index.d.ts +175 -53
- 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;
|
|
@@ -253,15 +253,20 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
255
|
* 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
|
-
|
|
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) {
|
|
258
259
|
for (const token of order){
|
|
259
260
|
const provider = providers.get(token);
|
|
260
261
|
if (!provider || !isFactoryProvider(provider)) continue;
|
|
262
|
+
if (control?.stopped()) return;
|
|
263
|
+
control?.pending(tokenName(token));
|
|
261
264
|
try {
|
|
262
265
|
await container.getAsync(token);
|
|
263
266
|
} catch (error) {
|
|
264
267
|
throw providerFailure(token, error);
|
|
268
|
+
} finally{
|
|
269
|
+
control?.pending(undefined);
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
}
|
|
@@ -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({
|
|
@@ -4,24 +4,22 @@ import './theme-scope.js';
|
|
|
4
4
|
import './theme-validation.js';
|
|
5
5
|
import '../common/decorator.js';
|
|
6
6
|
import 'reflect-metadata';
|
|
7
|
-
import '../common/
|
|
8
|
-
import '../common/
|
|
9
|
-
import '../util/interaction.util.js';
|
|
10
|
-
import '../common/response/flags.js';
|
|
11
|
-
import 'path';
|
|
12
|
-
import '../common/response/components.js';
|
|
7
|
+
import '../common/response/response-state.js';
|
|
8
|
+
import '../common/errors.js';
|
|
13
9
|
import { MemoryCooldownStore } from '../common/cooldown-store.js';
|
|
14
10
|
import 'node:crypto';
|
|
15
11
|
import '../decorator/controller.decorator.js';
|
|
16
12
|
import './scalar-types.js';
|
|
17
13
|
import { answerCooldown } from '../common/sharded-cooldown-store.js';
|
|
14
|
+
import '../common/translator.js';
|
|
15
|
+
import '../common/meocord-text.js';
|
|
18
16
|
import { registerCommands } from './command-registration.js';
|
|
19
17
|
import { SHUTDOWN_MARGIN_MS, shutdownTimeoutOf } from '../util/shutdown-timeout.util.js';
|
|
20
18
|
import { bundleEntry } from '../util/bundle-entry.util.js';
|
|
21
19
|
import { FORCE_REGISTER_ENV } from '../util/registration-mode.util.js';
|
|
22
20
|
import { isShardMessage, REFUSED_CODE } from './shard-messages.js';
|
|
23
21
|
import { tokenMessage, isRefusedToken } from './login-failure.js';
|
|
24
|
-
import { onDevRunnerStop,
|
|
22
|
+
import { onDevRunnerStop, tellDevRunner, underDevRunner } from '../util/dev-runner.util.js';
|
|
25
23
|
import { stopRequests } from '../util/stop-request.util.js';
|
|
26
24
|
|
|
27
25
|
/** How long the manager waits for a shard to become ready before spawning the next. */ const SHARD_READY_TIMEOUT_MS = 30000;
|
|
@@ -60,7 +58,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
60
58
|
process.on('SIGINT', ()=>void this.stopAndExit());
|
|
61
59
|
process.on('SIGTERM', ()=>void this.stopAndExit());
|
|
62
60
|
// How `meocord start --dev` stops the bot to restart it; no stop request of the user's, whose first Ctrl+C joins it
|
|
63
|
-
onDevRunnerStop(()=>void this.exitAfterShutdown());
|
|
61
|
+
onDevRunnerStop(()=>void this.exitAfterShutdown(), false);
|
|
64
62
|
let total;
|
|
65
63
|
try {
|
|
66
64
|
const shards = config.sharding?.shards ?? 'auto';
|
|
@@ -77,6 +75,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
77
75
|
const manager = (this.options.createManager ?? ((path, opts)=>new ShardingManager(path, opts)))(file, {
|
|
78
76
|
token,
|
|
79
77
|
totalShards: total,
|
|
78
|
+
// discord.js refuses a call to every shard unless shardList counts the shards; createShard leaves it 'auto'
|
|
79
|
+
shardList: [
|
|
80
|
+
...Array(total).keys()
|
|
81
|
+
],
|
|
80
82
|
mode: 'process',
|
|
81
83
|
execArgv: process.execArgv,
|
|
82
84
|
respawn: false
|
|
@@ -173,12 +175,12 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
173
175
|
void this.exitForLogin();
|
|
174
176
|
}
|
|
175
177
|
/** Exits 1 for a bot that could not log in, telling `meocord start --dev` first, which then waits for a change. */ exitForLogin() {
|
|
176
|
-
if (!
|
|
178
|
+
if (!this.devRunner) return this.exit(1);
|
|
177
179
|
// Set first, so the process exits 1 even if it ends while the message is still on its way
|
|
178
180
|
process.exitCode = 1;
|
|
179
181
|
return tellDevRunner({
|
|
180
182
|
meocord: 'login-failed'
|
|
181
|
-
}).then(()=>this.exit(1));
|
|
183
|
+
}, false).then(()=>this.exit(1));
|
|
182
184
|
}
|
|
183
185
|
/**
|
|
184
186
|
* Asks every shard to shut down through its own hooks, waits for them up to the shutdown timeout plus a margin, and
|
|
@@ -255,6 +257,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
255
257
|
/** Every shard's cooldown calls, for an app whose store is ShardedCooldownStore. */ this.cooldowns = new MemoryCooldownStore();
|
|
256
258
|
this.stopping = false;
|
|
257
259
|
this.fatal = false;
|
|
260
|
+
/**
|
|
261
|
+
* Whether `meocord start --dev` runs the manager, decided before discord.js's ShardingManager, which writes the
|
|
262
|
+
* variable that marks a shard into this process too.
|
|
263
|
+
*/ this.devRunner = underDevRunner(false);
|
|
258
264
|
// The manager holds the credential for its shards and logs their failures, without an app of its own to register it
|
|
259
265
|
hideInLogs(options.token);
|
|
260
266
|
this.exit = options.exit ?? ((code)=>process.exit(code));
|
|
@@ -3,6 +3,8 @@ import { injectedTokens, isGuardWithParams } from './guard-runner.js';
|
|
|
3
3
|
import { classDecorators, handlerStageClasses } from './handler-pipeline.js';
|
|
4
4
|
import { reachableClasses, assertTypedParameters } from './providers.js';
|
|
5
5
|
import { themeResolverClass } from './theme-resolvers.js';
|
|
6
|
+
import { isAppClassToken } from './lifecycle-order.js';
|
|
7
|
+
import { ExecutionContext } from '../common/execution-context.js';
|
|
6
8
|
import { META } from '../util/metadata-keys.js';
|
|
7
9
|
import { startupError } from '../util/refusal.util.js';
|
|
8
10
|
|
|
@@ -57,6 +59,24 @@ import { startupError } from '../util/refusal.util.js';
|
|
|
57
59
|
for (const [stage, decorator] of found)decorators.set(stage, decorator);
|
|
58
60
|
}
|
|
59
61
|
}
|
|
62
|
+
/** Whether making `cls` reaches the per-call `ExecutionContext` through the classes it injects, which each call makes. */ function reachesContext(cls, providers, seen = new Set()) {
|
|
63
|
+
seen.add(cls);
|
|
64
|
+
return injectedTokens(cls).some((token)=>token === ExecutionContext || isAppClassToken(token) && !providers.has(token) && !seen.has(token) && reachesContext(token, providers, seen));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
|
|
68
|
+
* app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
|
|
69
|
+
* made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
|
|
70
|
+
* is not made.
|
|
71
|
+
*/ function stageDependencies({ decorators }, providers, presenter) {
|
|
72
|
+
const stages = [
|
|
73
|
+
...decorators
|
|
74
|
+
].filter(([, decorator])=>decorator !== '@Controller()').map(([stage])=>stage);
|
|
75
|
+
if (typeof presenter === 'function') stages.push(presenter);
|
|
76
|
+
return [
|
|
77
|
+
...new Set(stages.flatMap((stage)=>injectedTokens(stage)))
|
|
78
|
+
].filter(isAppClassToken).filter((cls)=>!reachesContext(cls, providers));
|
|
79
|
+
}
|
|
60
80
|
/** How a container checks a guard first met at a call: the guard and what it injects, as the startup checks would. */ function lateGuardCheck(providers, translator) {
|
|
61
81
|
return (guard)=>assertStartupClasses({
|
|
62
82
|
classes: reachableClasses([
|
|
@@ -78,4 +98,4 @@ import { startupError } from '../util/refusal.util.js';
|
|
|
78
98
|
for (const needsOne of classes.filter((cls)=>injectedTokens(cls).includes(Translator)))startupError(missingTranslatorError(needsOne));
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
export { assertStartupClasses, lateGuardCheck, startupClasses };
|
|
101
|
+
export { assertStartupClasses, lateGuardCheck, stageDependencies, startupClasses };
|
|
@@ -118,7 +118,10 @@ const logger = new Logger('Theme');
|
|
|
118
118
|
*/ accept(id, result, report) {
|
|
119
119
|
if (result === undefined || result === null) return undefined;
|
|
120
120
|
const layer = copyLayer(result);
|
|
121
|
-
|
|
121
|
+
// A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
|
|
122
|
+
const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`, {
|
|
123
|
+
forLog: true
|
|
124
|
+
});
|
|
122
125
|
if (problems.length === 0) {
|
|
123
126
|
if (report) this.warned.delete(id);
|
|
124
127
|
return layer;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { DEFAULT_THEME } from './theme-defaults.js';
|
|
3
|
+
import { shared } from '../util/shared-state.util.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
// One per process for this meocord version, so a call's theme reads the same from its ES module and CommonJS builds
|
|
6
|
+
const scope = shared('themeScope', ()=>new AsyncLocalStorage());
|
|
5
7
|
const isPlainObject = (value)=>{
|
|
6
8
|
if (value === null || typeof value !== 'object') return false;
|
|
7
9
|
const prototype = Object.getPrototypeOf(value);
|