meocord 4.2.2 → 4.2.3
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 +130 -0
- package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
- package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
- package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
- package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +432 -181
- package/dist/cjs/decorator/index.cjs +85 -120
- package/dist/cjs/testing/index.cjs +1664 -784
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/package.json.template +9 -9
- package/dist/esm/bin/app-template/src/main.ts.template +3 -2
- package/dist/esm/bin/generator.js +6 -4
- package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
- package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
- package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
- package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
- package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
- package/dist/esm/bin/helper/service-generator.helper.js +6 -4
- package/dist/esm/bin/meocord.js +106 -48
- package/dist/esm/build/native-addons.js +75 -19
- package/dist/esm/build/rsbuild-config.js +11 -3
- package/dist/esm/build/typed-externals.js +15 -2
- package/dist/esm/common/cooldown-store.js +38 -14
- package/dist/esm/common/deprecation.js +4 -1
- package/dist/esm/common/execution-context.js +3 -1
- package/dist/esm/common/explained-error.js +7 -3
- package/dist/esm/common/log-level.js +13 -9
- package/dist/esm/common/logger.js +4 -2
- package/dist/esm/common/meocord-text.js +11 -6
- package/dist/esm/common/redis-cooldown-store.js +3 -7
- package/dist/esm/common/response/flags.js +17 -1
- package/dist/esm/common/response/install-context.js +27 -16
- package/dist/esm/common/response/presenter.js +7 -3
- package/dist/esm/common/response/response-state.js +27 -5
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +17 -8
- package/dist/esm/core/autocomplete-routes.js +26 -0
- package/dist/esm/core/command-conflicts.js +15 -7
- package/dist/esm/core/command-registration.js +15 -1
- package/dist/esm/core/cooldown-runner.js +7 -9
- package/dist/esm/core/defer.js +2 -5
- package/dist/esm/core/dispatcher.js +83 -58
- package/dist/esm/core/event-requirements.js +67 -19
- package/dist/esm/core/event-sources.js +100 -0
- package/dist/esm/core/fallback.js +3 -1
- package/dist/esm/core/filter-runner.js +4 -4
- package/dist/esm/core/guard-runner.js +173 -52
- package/dist/esm/core/handler-input.js +24 -3
- package/dist/esm/core/handler-pipeline.js +44 -28
- package/dist/esm/core/handler-registry.js +51 -21
- package/dist/esm/core/input-runner.js +7 -8
- package/dist/esm/core/interceptor-runner.js +4 -5
- package/dist/esm/core/lifecycle-hooks.js +3 -8
- package/dist/esm/core/lifecycle-order.js +1 -15
- package/dist/esm/core/meocord-classes.js +14 -1
- package/dist/esm/core/meocord-factory.js +80 -53
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +5 -9
- package/dist/esm/core/message-params.js +126 -46
- package/dist/esm/core/message-routes.js +225 -45
- package/dist/esm/core/message-words.js +30 -2
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +28 -3
- package/dist/esm/core/scalar-types.js +57 -16
- package/dist/esm/core/shard-context.js +11 -10
- package/dist/esm/core/shard-exit.js +3 -3
- package/dist/esm/core/shard-manager.js +16 -6
- package/dist/esm/core/startup-roots.js +28 -8
- package/dist/esm/core/theme-resolvers.js +26 -11
- package/dist/esm/core/theme-runtime.js +23 -13
- package/dist/esm/core/theme-scope.js +27 -29
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +6 -1
- package/dist/esm/decorator/command-builder.decorator.js +14 -0
- package/dist/esm/decorator/controller-class.decorator.js +1 -1
- package/dist/esm/decorator/controller.decorator.js +48 -19
- package/dist/esm/decorator/defer.decorator.js +2 -2
- package/dist/esm/decorator/event.decorator.js +4 -5
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +4 -61
- package/dist/esm/decorator/inject.decorator.js +4 -1
- package/dist/esm/decorator/interceptor.decorator.js +2 -2
- package/dist/esm/decorator/theme.decorator.js +4 -3
- package/dist/esm/decorator/validation.decorator.js +5 -5
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +29 -13
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -355
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +195 -77
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +639 -113
- package/dist/esm/testing/response-log.js +8 -0
- package/dist/esm/testing/response.js +7 -5
- package/dist/esm/testing/routing.js +48 -6
- package/dist/esm/testing/strict-mocks.js +1 -0
- package/dist/esm/testing/theme.js +3 -2
- package/dist/esm/util/common.util.js +30 -9
- package/dist/esm/util/generator-cli.util.js +6 -4
- package/dist/esm/util/inherited-env.util.js +72 -13
- package/dist/esm/util/json.util.js +10 -3
- package/dist/esm/util/meocord-cli.util.js +3 -2
- package/dist/esm/util/meocord-config-loader.util.js +14 -1
- package/dist/esm/util/meocord-config-validation.util.js +13 -3
- package/dist/esm/util/meocord-source-config.util.js +13 -3
- package/dist/esm/util/platform.util.js +3 -2
- package/dist/esm/util/refusal.util.js +29 -11
- package/dist/esm/util/registration-mode.util.js +14 -1
- package/dist/esm/util/timer-limit.util.js +4 -1
- package/dist/esm/util/tsconfig-paths.util.js +38 -7
- package/dist/esm/util/tsconfig.util.js +16 -58
- package/dist/esm/util/user-text.util.js +8 -4
- package/dist/esm/util/value.util.js +15 -4
- package/dist/types/common/index.d.cts +13 -9
- package/dist/types/common/index.d.ts +13 -9
- package/dist/types/core/index.d.cts +21 -11
- package/dist/types/core/index.d.ts +21 -11
- package/dist/types/decorator/index.d.cts +63 -63
- package/dist/types/decorator/index.d.ts +63 -63
- package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
- package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
- package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
- package/dist/types/testing/index.d.cts +137 -54
- package/dist/types/testing/index.d.ts +137 -54
- package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
- package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
- package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
- package/package.json +5 -2
package/dist/cjs/core/index.cjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
require('reflect-metadata');
|
|
4
|
-
var providers = require('../_shared/providers-
|
|
4
|
+
var providers = require('../_shared/providers-BBvbnmLP.cjs');
|
|
5
5
|
var inversify = require('inversify');
|
|
6
6
|
var discord_js = require('discord.js');
|
|
7
|
-
var controller_decorator = require('../_shared/controller.decorator-
|
|
7
|
+
var controller_decorator = require('../_shared/controller.decorator-hp5xdJU4.cjs');
|
|
8
8
|
require('node:crypto');
|
|
9
|
-
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-
|
|
10
|
-
var
|
|
9
|
+
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-B62g5O88.cjs');
|
|
10
|
+
var eventSources = require('../_shared/event-sources-DzqTODXg.cjs');
|
|
11
11
|
var enum_index = require('../enum/index.cjs');
|
|
12
12
|
var node_fs = require('node:fs');
|
|
13
13
|
var path = require('node:path');
|
|
@@ -17,13 +17,13 @@ require('crypto');
|
|
|
17
17
|
require('fs');
|
|
18
18
|
require('path');
|
|
19
19
|
require('cli-table3');
|
|
20
|
-
require('node:url');
|
|
21
20
|
require('dayjs');
|
|
22
21
|
require('dayjs/plugin/utc.js');
|
|
23
22
|
require('dayjs/plugin/timezone.js');
|
|
24
23
|
require('module');
|
|
25
24
|
require('chalk');
|
|
26
|
-
require('
|
|
25
|
+
require('node:url');
|
|
26
|
+
require('../_shared/translate-error-wsgfsUET.cjs');
|
|
27
27
|
|
|
28
28
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
29
29
|
|
|
@@ -42,6 +42,22 @@ const REACTION_PARTIALS = [
|
|
|
42
42
|
discord_js.Partials.Message,
|
|
43
43
|
discord_js.Partials.Reaction
|
|
44
44
|
];
|
|
45
|
+
const POLLS = [
|
|
46
|
+
discord_js.GatewayIntentBits.GuildMessagePolls,
|
|
47
|
+
discord_js.GatewayIntentBits.DirectMessagePolls
|
|
48
|
+
];
|
|
49
|
+
// discord.js drops a vote on a message it has not cached unless it may build the message, its poll and the answer
|
|
50
|
+
const POLL_PARTIALS = [
|
|
51
|
+
discord_js.Partials.Message,
|
|
52
|
+
discord_js.Partials.Poll,
|
|
53
|
+
discord_js.Partials.PollAnswer
|
|
54
|
+
];
|
|
55
|
+
// The partials whose warning names members and polls; the others keep the wording for messages, reactions and users
|
|
56
|
+
const CACHED_ELSEWHERE = new Set([
|
|
57
|
+
discord_js.Partials.GuildMember,
|
|
58
|
+
discord_js.Partials.Poll,
|
|
59
|
+
discord_js.Partials.PollAnswer
|
|
60
|
+
]);
|
|
45
61
|
/** The intents Discord only sends once they are also enabled for the app in the developer portal. */ const PRIVILEGED = new Set([
|
|
46
62
|
discord_js.GatewayIntentBits.GuildMembers,
|
|
47
63
|
discord_js.GatewayIntentBits.GuildPresences,
|
|
@@ -79,10 +95,9 @@ const REACTION_PARTIALS = [
|
|
|
79
95
|
]
|
|
80
96
|
}
|
|
81
97
|
])),
|
|
98
|
+
// A member that joins arrives whole; one that leaves or changes reaches its handler only if cached or with this partial
|
|
82
99
|
...Object.fromEntries([
|
|
83
100
|
'guildMemberAdd',
|
|
84
|
-
'guildMemberRemove',
|
|
85
|
-
'guildMemberUpdate',
|
|
86
101
|
'guildMemberAvailable',
|
|
87
102
|
'threadMembersUpdate'
|
|
88
103
|
].map((event)=>[
|
|
@@ -95,6 +110,26 @@ const REACTION_PARTIALS = [
|
|
|
95
110
|
]
|
|
96
111
|
}
|
|
97
112
|
])),
|
|
113
|
+
guildMemberRemove: {
|
|
114
|
+
intents: [
|
|
115
|
+
[
|
|
116
|
+
discord_js.GatewayIntentBits.GuildMembers
|
|
117
|
+
]
|
|
118
|
+
],
|
|
119
|
+
partials: [
|
|
120
|
+
discord_js.Partials.GuildMember
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
guildMemberUpdate: {
|
|
124
|
+
intents: [
|
|
125
|
+
[
|
|
126
|
+
discord_js.GatewayIntentBits.GuildMembers
|
|
127
|
+
]
|
|
128
|
+
],
|
|
129
|
+
partials: [
|
|
130
|
+
discord_js.Partials.GuildMember
|
|
131
|
+
]
|
|
132
|
+
},
|
|
98
133
|
guildAuditLogEntryCreate: {
|
|
99
134
|
intents: [
|
|
100
135
|
[
|
|
@@ -227,9 +262,7 @@ const REACTION_PARTIALS = [
|
|
|
227
262
|
intents: [
|
|
228
263
|
REACTIONS
|
|
229
264
|
],
|
|
230
|
-
partials:
|
|
231
|
-
discord_js.Partials.Reaction
|
|
232
|
-
]
|
|
265
|
+
partials: REACTION_PARTIALS
|
|
233
266
|
},
|
|
234
267
|
typingStart: {
|
|
235
268
|
intents: [
|
|
@@ -241,26 +274,20 @@ const REACTION_PARTIALS = [
|
|
|
241
274
|
},
|
|
242
275
|
messagePollVoteAdd: {
|
|
243
276
|
intents: [
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
]
|
|
248
|
-
]
|
|
277
|
+
POLLS
|
|
278
|
+
],
|
|
279
|
+
partials: POLL_PARTIALS
|
|
249
280
|
},
|
|
250
281
|
messagePollVoteRemove: {
|
|
251
282
|
intents: [
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
]
|
|
256
|
-
]
|
|
283
|
+
POLLS
|
|
284
|
+
],
|
|
285
|
+
partials: POLL_PARTIALS
|
|
257
286
|
},
|
|
258
287
|
...Object.fromEntries([
|
|
259
288
|
'guildScheduledEventCreate',
|
|
260
289
|
'guildScheduledEventDelete',
|
|
261
|
-
'guildScheduledEventUpdate'
|
|
262
|
-
'guildScheduledEventUserAdd',
|
|
263
|
-
'guildScheduledEventUserRemove'
|
|
290
|
+
'guildScheduledEventUpdate'
|
|
264
291
|
].map((event)=>[
|
|
265
292
|
event,
|
|
266
293
|
{
|
|
@@ -271,6 +298,27 @@ const REACTION_PARTIALS = [
|
|
|
271
298
|
]
|
|
272
299
|
}
|
|
273
300
|
])),
|
|
301
|
+
// Each names its user by id alone, which discord.js reads from the cache unless this partial is on
|
|
302
|
+
guildScheduledEventUserAdd: {
|
|
303
|
+
intents: [
|
|
304
|
+
[
|
|
305
|
+
discord_js.GatewayIntentBits.GuildScheduledEvents
|
|
306
|
+
]
|
|
307
|
+
],
|
|
308
|
+
partials: [
|
|
309
|
+
discord_js.Partials.User
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
guildScheduledEventUserRemove: {
|
|
313
|
+
intents: [
|
|
314
|
+
[
|
|
315
|
+
discord_js.GatewayIntentBits.GuildScheduledEvents
|
|
316
|
+
]
|
|
317
|
+
],
|
|
318
|
+
partials: [
|
|
319
|
+
discord_js.Partials.User
|
|
320
|
+
]
|
|
321
|
+
},
|
|
274
322
|
autoModerationRuleCreate: {
|
|
275
323
|
intents: [
|
|
276
324
|
[
|
|
@@ -384,7 +432,7 @@ function intentName(bit) {
|
|
|
384
432
|
(labels.some((label)=>label.startsWith("@MessageHandler('")) ? "; messages: { mention: 'only' } needs none." : '.') : ''));
|
|
385
433
|
}
|
|
386
434
|
for (const [partial, labels] of missingPartials){
|
|
387
|
-
warnings.push(`Partials.${discord_js.Partials[partial]} is not in clientOptions.partials, so ${labels.join(', ')} will miss ` + (partial === discord_js.Partials.Channel ? 'direct messages: no DM channel is cached after the bot starts, and discord.js drops a message from a ' + 'channel it has not cached.' : 'events about messages, reactions or users the bot has not cached, such as messages sent before it started.'));
|
|
435
|
+
warnings.push(`Partials.${discord_js.Partials[partial]} is not in clientOptions.partials, so ${labels.join(', ')} will miss ` + (partial === discord_js.Partials.Channel ? 'direct messages: no DM channel is cached after the bot starts, and discord.js drops a message from a ' + 'channel it has not cached.' : CACHED_ELSEWHERE.has(partial) ? 'events about members or polls the bot has not cached, such as members who joined before it started.' : 'events about messages, reactions or users the bot has not cached, such as messages sent before it started.'));
|
|
388
436
|
}
|
|
389
437
|
return warnings;
|
|
390
438
|
}
|
|
@@ -405,6 +453,21 @@ function intentName(bit) {
|
|
|
405
453
|
};
|
|
406
454
|
}
|
|
407
455
|
|
|
456
|
+
/** Where both builds of this version keep the process's state, as {@link shared} names it. */ const PROCESS_STATE = 'processState';
|
|
457
|
+
/**
|
|
458
|
+
* What the process's apps share, kept by both builds of this version, so a process that loads both has one pair of
|
|
459
|
+
* signal listeners and one shutdown, which stops the apps either build started, one exit code a failed start set, and
|
|
460
|
+
* one end of a failed shard.
|
|
461
|
+
*/ const processState = controller_decorator.shared(PROCESS_STATE, ()=>({
|
|
462
|
+
/** Closes each started app: runs its shutdown hooks and destroys its client, resolving `false` on failure. */ runningApps: new Set(),
|
|
463
|
+
stopRequest: stopRequests(),
|
|
464
|
+
signalHandlersInstalled: false,
|
|
465
|
+
shuttingDown: undefined,
|
|
466
|
+
/** Whether a failed start, of a provider's factory or the login, set the exit code, so a later login knows to clear it. */ startFailureSetExitCode: false,
|
|
467
|
+
/** Whether `meocord start --dev` was told the bot could not log in, so a later login tells it the bot is online. */ toldDevRunnerLoginFailed: false,
|
|
468
|
+
/** Whether this shard is already ending, so a failure reported twice, as one going on uncaught is, ends it once. */ shardEnding: false
|
|
469
|
+
}));
|
|
470
|
+
|
|
408
471
|
/** How long a message to the parent process may take to be sent before the parent is taken to be gone. */ const PARENT_SEND_TIMEOUT_MS = 1000;
|
|
409
472
|
/**
|
|
410
473
|
* Sends a message over the IPC channel to the parent process and waits until it is sent, or `PARENT_SEND_TIMEOUT_MS`:
|
|
@@ -516,14 +579,13 @@ const slots = globalThis;
|
|
|
516
579
|
*/ async function tellManager(message) {
|
|
517
580
|
await sendToParent(message);
|
|
518
581
|
}
|
|
519
|
-
/** Whether this shard is already ending, so a failure reported twice, as one going on uncaught is, ends it once. */ let ending = false;
|
|
520
582
|
/**
|
|
521
583
|
* Ends a shard whose startup failed, which its manager restarts unless it was told the failure is one every shard
|
|
522
584
|
* meets: a refusal, told here first, which the manager logs before stopping them all, or a login Discord refused, told
|
|
523
585
|
* as it failed. The exit waits until the caller has handled the error and the manager has the reason.
|
|
524
586
|
*/ function endFailedShard(error) {
|
|
525
|
-
if (
|
|
526
|
-
|
|
587
|
+
if (processState.shardEnding) return;
|
|
588
|
+
processState.shardEnding = true;
|
|
527
589
|
const refused = controller_decorator.isRefusal(error);
|
|
528
590
|
const reason = refused && controller_decorator.describeRefusal(error, process.cwd());
|
|
529
591
|
if (refused) {
|
|
@@ -624,21 +686,31 @@ const slots = globalThis;
|
|
|
624
686
|
*/ /** Set by `meocord register`: register the commands over REST and exit, without logging in. */ const REGISTER_ONLY_ENV = 'MEOCORD_REGISTER_ONLY';
|
|
625
687
|
/** Set by `meocord register --guild <id>`: register every command to that guild only. */ const REGISTER_GUILD_ENV = 'MEOCORD_REGISTER_GUILD';
|
|
626
688
|
/** Set by `meocord start --force-register` and by `meocord register`: send even an unchanged payload. */ const FORCE_REGISTER_ENV = 'MEOCORD_FORCE_REGISTER';
|
|
689
|
+
/**
|
|
690
|
+
* Set by the CLI for every bot it starts: `development` or `production`, the mode the command runs in. A bundle built
|
|
691
|
+
* with `bundleDependencies` has `process.env.NODE_ENV` written in as the mode it was built in, MeoCord's own reads
|
|
692
|
+
* included, so the command's mode reaches it through this variable instead.
|
|
693
|
+
*/ const REGISTER_MODE_ENV = 'MEOCORD_REGISTER_MODE';
|
|
694
|
+
/**
|
|
695
|
+
* Whether commands register as development does, to `commands.developmentGuild`: as the CLI command that started the
|
|
696
|
+
* bot says, else as `NODE_ENV` says.
|
|
697
|
+
*/ function registersAsDevelopment(env = process.env) {
|
|
698
|
+
const mode = env[REGISTER_MODE_ENV];
|
|
699
|
+
if (mode === 'development' || mode === 'production') return mode === 'development';
|
|
700
|
+
return process.env.NODE_ENV === 'development';
|
|
701
|
+
}
|
|
627
702
|
/** Whether this process was started to register commands and exit. */ function isRegisterOnly(env = process.env) {
|
|
628
703
|
return env[REGISTER_ONLY_ENV] === '1';
|
|
629
704
|
}
|
|
630
705
|
|
|
631
706
|
/** How long an `onReady` hook runs before a warning says the hooks after it are waiting. */ const SLOW_READY_HOOK_MS = 10000;
|
|
632
|
-
|
|
633
|
-
const stopRequest = stopRequests();
|
|
634
|
-
let signalHandlersInstalled = false;
|
|
635
|
-
let shuttingDown;
|
|
707
|
+
const { runningApps } = processState;
|
|
636
708
|
/**
|
|
637
|
-
* Shuts every started app down and exits, with
|
|
638
|
-
* it, and in a shard its manager's request or its going. A call within `REPEAT_SIGNAL_WINDOW_MS` of the first is the
|
|
709
|
+
* Shuts every started app down and exits, with the code set when the shutdown began or 0, and 1 if an app failed to
|
|
710
|
+
* close. Signals call it, and in a shard its manager's request or its going. A call within `REPEAT_SIGNAL_WINDOW_MS` of the first is the
|
|
639
711
|
* same request; one after it forces exit 1, except in a shard, whose manager forces it.
|
|
640
712
|
*/ async function shutdownAndExit(shard) {
|
|
641
|
-
const request = stopRequest();
|
|
713
|
+
const request = processState.stopRequest();
|
|
642
714
|
if (request !== 'first') {
|
|
643
715
|
// A shard hears Ctrl+C both directly and from its manager, which owns forcing it; so it waits
|
|
644
716
|
if (request === 'duplicate' || shard) return;
|
|
@@ -648,23 +720,28 @@ let shuttingDown;
|
|
|
648
720
|
return shutDown();
|
|
649
721
|
}
|
|
650
722
|
/** The one shutdown of the process, however many ask for it: a stop request joins one already under way. */ function shutDown() {
|
|
651
|
-
|
|
723
|
+
var _processState;
|
|
724
|
+
(_processState = processState).shuttingDown ?? (_processState.shuttingDown = (async ()=>{
|
|
725
|
+
// A clean stop keeps a code already set, such as the 1 a failed login set, so a supervisor still sees it. Read as
|
|
726
|
+
// the shutdown begins: a code main.ts sets as it hears of the stop is the stop's, not a failure.
|
|
727
|
+
const exitCode = Number(process.exitCode ?? 0) || 0;
|
|
652
728
|
const closed = await Promise.all([
|
|
653
729
|
...runningApps
|
|
654
730
|
].map((close)=>close()));
|
|
655
|
-
|
|
656
|
-
process.exit(closed.every(Boolean) ? Number(process.exitCode ?? 0) || 0 : 1);
|
|
731
|
+
process.exit(closed.every(Boolean) ? exitCode : 1);
|
|
657
732
|
})());
|
|
658
|
-
return shuttingDown;
|
|
733
|
+
return processState.shuttingDown;
|
|
659
734
|
}
|
|
735
|
+
/** The errors {@link stoppedBeforeOnline} made, which end a start without being its failure. */ const stops = new WeakSet();
|
|
660
736
|
/** What `start()` rejects with when shutdown begins before the bot came online, which shutdown has logged. */ function stoppedBeforeOnline() {
|
|
661
737
|
const error = new Error('The bot was stopped before it came online.');
|
|
662
738
|
controller_decorator.markExplained(error);
|
|
739
|
+
stops.add(error);
|
|
663
740
|
return error;
|
|
664
741
|
}
|
|
665
742
|
/** One pair of signal listeners for the process, however many apps it starts, for the role the first decided. */ function installSignalHandlers(shard) {
|
|
666
|
-
if (signalHandlersInstalled) return;
|
|
667
|
-
signalHandlersInstalled = true;
|
|
743
|
+
if (processState.signalHandlersInstalled) return;
|
|
744
|
+
processState.signalHandlersInstalled = true;
|
|
668
745
|
process.on('SIGINT', ()=>void shutdownAndExit(shard));
|
|
669
746
|
process.on('SIGTERM', ()=>void shutdownAndExit(shard));
|
|
670
747
|
// How `meocord start --dev` stops the application to restart it, a signal being no graceful stop on Windows. It is
|
|
@@ -717,13 +794,21 @@ class MeoCordApp {
|
|
|
717
794
|
}
|
|
718
795
|
}
|
|
719
796
|
/**
|
|
720
|
-
* Logs the bot in, as {@link MeoCordApplication.start} describes. The
|
|
721
|
-
*
|
|
797
|
+
* Logs the bot in, as {@link MeoCordApplication.start} describes. The event handlers are attached once; the providers
|
|
798
|
+
* and services are made for each start, as a failed one, of a factory or the login, shuts them down and drops them.
|
|
722
799
|
*/ async start() {
|
|
723
800
|
if (isRegisterOnly()) return this.registerOnly();
|
|
724
801
|
if (this.stopped) throw new Error('This app was stopped; use MeoCordFactory.create to make a new one.');
|
|
725
802
|
if (this.online) return;
|
|
726
|
-
this.starting ?? (this.starting = this.startOnce().catch((error)=>{
|
|
803
|
+
this.starting ?? (this.starting = this.startOnce().catch(async (error)=>{
|
|
804
|
+
// A failed start stays among the running apps while it shuts down what it made, which a stop meanwhile waits
|
|
805
|
+
// for, and then while it holds a value it was given open, which a signal closes as stop() does. One that holds
|
|
806
|
+
// nothing leaves, so a signal later stops only the apps that run; a stopped start stays until its close settles
|
|
807
|
+
if (!this.closing) {
|
|
808
|
+
this.undoing = this.undoStart().finally(()=>this.undoing = undefined);
|
|
809
|
+
await this.undoing;
|
|
810
|
+
if (!this.closing && !this.holdsOpen()) runningApps.delete(this.close);
|
|
811
|
+
}
|
|
727
812
|
// A shard ends, for its manager to restart, or to stop all if told it is fatal; a stopped start is the stop's
|
|
728
813
|
if (this.shard && !this.closing) endFailedShard(error);
|
|
729
814
|
throw error;
|
|
@@ -744,6 +829,30 @@ class MeoCordApp {
|
|
|
744
829
|
// As a signal's shutdown exits 1 for a client that failed to close, a code another failure set aside
|
|
745
830
|
if (!await this.close() && !process.exitCode) process.exitCode = 1;
|
|
746
831
|
}
|
|
832
|
+
/**
|
|
833
|
+
* Shuts down what a failed start made, by the rule a stop follows, and drops it from the container, so the process
|
|
834
|
+
* holds nothing of it open and a retry makes it afresh.
|
|
835
|
+
*/ async undoStart() {
|
|
836
|
+
if (!this.attempt || !this.begun) return;
|
|
837
|
+
this.begun = false;
|
|
838
|
+
const { given, constructed } = this.attempt;
|
|
839
|
+
const entries = this.shutdownEntries().filter(({ token })=>!given.has(token));
|
|
840
|
+
// A value the app gave stays constructed, so a stop with no retry still closes it
|
|
841
|
+
for (const token of constructed)if (!given.has(token)) constructed.delete(token);
|
|
842
|
+
this.readySettled.clear();
|
|
843
|
+
this.attempt.drop();
|
|
844
|
+
this.made = false;
|
|
845
|
+
// Nothing came online, so no call runs and no listener needs removing
|
|
846
|
+
await eventSources.runShutdownSequence(this.container, ()=>entries, {
|
|
847
|
+
drainCalls: ()=>this.calls.settled(),
|
|
848
|
+
timeoutMs: this.shutdownTimeout,
|
|
849
|
+
hookFailed: (name, error)=>this.logger.error(`onShutdown failed in ${name}:`, error),
|
|
850
|
+
warn: (message)=>this.logger.warn(message)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/** Whether a shutdown has an `onShutdown` to run, as for a value the app gave once a failed start is undone. */ holdsOpen() {
|
|
854
|
+
return this.shutdownEntries().some(({ instance })=>typeof instance.onShutdown === 'function');
|
|
855
|
+
}
|
|
747
856
|
async startOnce() {
|
|
748
857
|
this.logger.log('Starting bot...');
|
|
749
858
|
// Before the providers are made, so a signal, or a shard's manager, can stop a start that is slow to come online
|
|
@@ -754,7 +863,9 @@ class MeoCordApp {
|
|
|
754
863
|
// A bot runs one app, whose theme code outside any call reads, its providers' factories included; claimed for
|
|
755
864
|
// each start, as a failed one gives it up
|
|
756
865
|
providers.claimAmbientAppTheme(this.container);
|
|
757
|
-
if (!this.
|
|
866
|
+
if (!this.made) {
|
|
867
|
+
this.attempt?.begin();
|
|
868
|
+
this.begun = true;
|
|
758
869
|
// Kept while it runs, so a stop meanwhile waits for it
|
|
759
870
|
this.preparing = this.prepare();
|
|
760
871
|
try {
|
|
@@ -762,7 +873,11 @@ class MeoCordApp {
|
|
|
762
873
|
} finally{
|
|
763
874
|
this.preparing = undefined;
|
|
764
875
|
}
|
|
765
|
-
this.
|
|
876
|
+
this.made = true;
|
|
877
|
+
}
|
|
878
|
+
if (!this.attached) {
|
|
879
|
+
this.attachHandlers();
|
|
880
|
+
this.attached = true;
|
|
766
881
|
}
|
|
767
882
|
if (this.loginFailed) {
|
|
768
883
|
controller_decorator.warnDeprecatedBehaviour(this.logger, 'Retrying start() after a failed login', 'rejects', 'MeoCordFactory.create to make a new app');
|
|
@@ -770,7 +885,7 @@ class MeoCordApp {
|
|
|
770
885
|
}
|
|
771
886
|
await this.login();
|
|
772
887
|
}
|
|
773
|
-
/** Makes the provided values and listed services
|
|
888
|
+
/** Makes the provided values and listed services. */ async prepare() {
|
|
774
889
|
// Every provided value is made before anything that injects it is resolved, and before login
|
|
775
890
|
if (this.startup) {
|
|
776
891
|
try {
|
|
@@ -782,7 +897,7 @@ class MeoCordApp {
|
|
|
782
897
|
providers.releaseAmbientAppTheme(this.container);
|
|
783
898
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
784
899
|
process.exitCode = 1;
|
|
785
|
-
|
|
900
|
+
processState.startFailureSetExitCode = true;
|
|
786
901
|
}
|
|
787
902
|
throw error;
|
|
788
903
|
}
|
|
@@ -795,6 +910,8 @@ class MeoCordApp {
|
|
|
795
910
|
this.storeReady = Promise.withResolvers();
|
|
796
911
|
providers.waitForCooldownStore(this.container, this.storeReady.promise);
|
|
797
912
|
}
|
|
913
|
+
}
|
|
914
|
+
/** Attaches the Discord event handlers. */ attachHandlers() {
|
|
798
915
|
this.bot.on('clientReady', (readyClient)=>this.runListener('clientReady', async ()=>{
|
|
799
916
|
// Only for activities the app listed: without them, a status it sets itself stays as it set it
|
|
800
917
|
if (this.activities?.length) {
|
|
@@ -807,7 +924,7 @@ class MeoCordApp {
|
|
|
807
924
|
if (!this.shard) await this.registerCommands();
|
|
808
925
|
await readyHooks;
|
|
809
926
|
}));
|
|
810
|
-
this.bot.on('interactionCreate',
|
|
927
|
+
this.bot.on('interactionCreate', eventSources.ownInteractionListener((interaction)=>this.runListener('interactionCreate', ()=>this.dispatcher.interaction(interaction), interactionLabel(interaction))));
|
|
811
928
|
this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.dispatcher.message(message)));
|
|
812
929
|
this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.dispatcher.reaction(reaction, {
|
|
813
930
|
user,
|
|
@@ -819,9 +936,10 @@ class MeoCordApp {
|
|
|
819
936
|
})));
|
|
820
937
|
// discord.js drops a reaction in a DM channel it has not cached; this fetches the channel and delivers it
|
|
821
938
|
deliverUncachedDmReactions(this.bot, this.logger);
|
|
822
|
-
this.
|
|
823
|
-
this.
|
|
824
|
-
this.
|
|
939
|
+
const sources = this.eventSources();
|
|
940
|
+
this.attachEventHandlers(sources);
|
|
941
|
+
this.warnAboutMissingRequirements(sources);
|
|
942
|
+
this.noteGlobalStagesOnEvents(sources);
|
|
825
943
|
}
|
|
826
944
|
/** Logs the bot in, setting the exit code when that fails and clearing it when a later attempt succeeds. */ async login() {
|
|
827
945
|
// Checked here as well, after the last await before the login: from here to setting abortLogin runs in one turn,
|
|
@@ -842,7 +960,6 @@ class MeoCordApp {
|
|
|
842
960
|
]);
|
|
843
961
|
} catch (error) {
|
|
844
962
|
if (this.closing) throw error;
|
|
845
|
-
runningApps.delete(this.close);
|
|
846
963
|
// A bot that never came online is not the app whose theme is read outside calls
|
|
847
964
|
providers.releaseAmbientAppTheme(this.container);
|
|
848
965
|
const fatal = fatalLoginCode(error);
|
|
@@ -857,12 +974,12 @@ class MeoCordApp {
|
|
|
857
974
|
if (fatal) await reportFatalLogin(this.shard, fatal, explanation ?? (error instanceof Error ? error.message : String(error)));
|
|
858
975
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
859
976
|
process.exitCode = 1;
|
|
860
|
-
|
|
977
|
+
processState.startFailureSetExitCode = true;
|
|
861
978
|
}
|
|
862
979
|
await tellDevRunner({
|
|
863
980
|
meocord: 'login-failed'
|
|
864
981
|
}, this.shard);
|
|
865
|
-
|
|
982
|
+
processState.toldDevRunnerLoginFailed = true;
|
|
866
983
|
this.loginFailed = true;
|
|
867
984
|
throw error;
|
|
868
985
|
} finally{
|
|
@@ -871,12 +988,12 @@ class MeoCordApp {
|
|
|
871
988
|
// A shutdown that began as the login completed has the client to close; the bot is not reported online
|
|
872
989
|
if (this.closing) throw stoppedBeforeOnline();
|
|
873
990
|
// 0 rather than undefined, which Bun ignores
|
|
874
|
-
if (
|
|
991
|
+
if (processState.startFailureSetExitCode && process.exitCode === 1) {
|
|
875
992
|
process.exitCode = 0;
|
|
876
|
-
|
|
993
|
+
processState.startFailureSetExitCode = false;
|
|
877
994
|
}
|
|
878
|
-
if (
|
|
879
|
-
|
|
995
|
+
if (processState.toldDevRunnerLoginFailed) {
|
|
996
|
+
processState.toldDevRunnerLoginFailed = false;
|
|
880
997
|
await tellDevRunner({
|
|
881
998
|
meocord: 'online'
|
|
882
999
|
}, this.shard);
|
|
@@ -886,11 +1003,16 @@ class MeoCordApp {
|
|
|
886
1003
|
}
|
|
887
1004
|
/**
|
|
888
1005
|
* Registers the application's commands with Discord, where `meocord.config.ts`'s `commands` says. Runs once the bot
|
|
889
|
-
* is ready, except in a shard, whose manager registers.
|
|
1006
|
+
* is ready, except in a shard, whose manager registers. Called before the bot is online, it logs a warning and
|
|
1007
|
+
* registers nothing, as the application comes from the login. It never throws: a failure is logged and the bot stays
|
|
890
1008
|
* online.
|
|
891
1009
|
*/ async registerCommands() {
|
|
1010
|
+
// discord.js sets the application when the gateway's READY arrives
|
|
892
1011
|
const applicationId = this.bot.application?.id;
|
|
893
|
-
if (!applicationId)
|
|
1012
|
+
if (!applicationId) {
|
|
1013
|
+
this.logger.warn('registerCommands() registers nothing before the bot is online, as Discord names the application at login. ' + 'The bot registers once it is online; to register without logging in, run `meocord register`.');
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
894
1016
|
const config = controller_decorator.loadMeoCordConfig()?.commands;
|
|
895
1017
|
if (config?.register === false) {
|
|
896
1018
|
this.logger.log('Command registration is off (commands.register: false); run `meocord register` to register.');
|
|
@@ -902,7 +1024,7 @@ class MeoCordApp {
|
|
|
902
1024
|
controllerClasses: this.controllerClasses,
|
|
903
1025
|
logger: this.logger,
|
|
904
1026
|
config,
|
|
905
|
-
development:
|
|
1027
|
+
development: registersAsDevelopment(),
|
|
906
1028
|
force: process.env[FORCE_REGISTER_ENV] === '1'
|
|
907
1029
|
});
|
|
908
1030
|
}
|
|
@@ -933,24 +1055,25 @@ class MeoCordApp {
|
|
|
933
1055
|
controllerClasses: this.controllerClasses,
|
|
934
1056
|
logger: this.logger,
|
|
935
1057
|
config: controller_decorator.loadMeoCordConfig()?.commands,
|
|
936
|
-
development:
|
|
1058
|
+
development: registersAsDevelopment(),
|
|
937
1059
|
onlyGuild: process.env[REGISTER_GUILD_ENV],
|
|
938
1060
|
force: true
|
|
939
1061
|
});
|
|
940
1062
|
process.exit(registered ? 0 : 1);
|
|
941
1063
|
}
|
|
942
1064
|
/**
|
|
943
|
-
* Adds a client listener for every `@On` and `@Once` handler on
|
|
944
|
-
*
|
|
945
|
-
* handler, and the next listener still runs.
|
|
946
|
-
*/ attachEventHandlers() {
|
|
947
|
-
for (const
|
|
948
|
-
|
|
949
|
-
|
|
1065
|
+
* Adds a client listener for every `@On` and `@Once` handler on each instance whose lifecycle hooks run, in lifecycle
|
|
1066
|
+
* order, each event run on the instance the container holds. Each call is isolated: an error is logged against the
|
|
1067
|
+
* event and the handler, and the next listener still runs.
|
|
1068
|
+
*/ attachEventHandlers(sources) {
|
|
1069
|
+
for (const source of sources){
|
|
1070
|
+
const { name } = source.cls;
|
|
1071
|
+
for (const { event, method, once } of source.handlers){
|
|
1072
|
+
const logError = (error)=>this.logger.error(`Error handling event "${event}" in ${name}.${method}:`, error);
|
|
950
1073
|
// An error no filter handles is logged, with the handler. A guard denying an event only filters which
|
|
951
1074
|
// events the handler takes, and a UserError is meant for the sender of the event's message, if it has one
|
|
952
1075
|
const fallback = async (error, context)=>{
|
|
953
|
-
const where = `event "${event}" in ${
|
|
1076
|
+
const where = `event "${event}" in ${name}.${method}`;
|
|
954
1077
|
if (error instanceof controller_decorator.GuardDeniedError) this.logger.debug(`Denied ${where}: ${controller_decorator.escapeForLog(error.message)}`);
|
|
955
1078
|
else if (error instanceof controller_decorator.UserError) {
|
|
956
1079
|
this.logger.debug(`Refused ${where}: ${controller_decorator.escapeForLog(error.message)}`);
|
|
@@ -958,16 +1081,16 @@ class MeoCordApp {
|
|
|
958
1081
|
const message = [
|
|
959
1082
|
...context.getArgs()
|
|
960
1083
|
].reverse().find((arg)=>arg instanceof discord_js.Message);
|
|
961
|
-
if (message) return
|
|
1084
|
+
if (message) return eventSources.replyWithUserError(message, error, this.logger, this.messageOptions?.replyEmoji);
|
|
962
1085
|
} else logError(error);
|
|
963
1086
|
return undefined;
|
|
964
1087
|
};
|
|
965
|
-
const label = `@${once ? 'Once' : 'On'}('${event}') in ${
|
|
1088
|
+
const label = `@${once ? 'Once' : 'On'}('${event}') in ${name}.${method}`;
|
|
966
1089
|
// A call, as a dispatched one is, which shutdown waits for
|
|
967
1090
|
const listener = (...args)=>this.calls.run(label, async ()=>{
|
|
968
1091
|
try {
|
|
969
|
-
const instance =
|
|
970
|
-
await providers.runHandler(this.container, instance, method, args, {
|
|
1092
|
+
const instance = source.resolve();
|
|
1093
|
+
if (instance) await providers.runHandler(this.container, instance, method, args, {
|
|
971
1094
|
fallback,
|
|
972
1095
|
type: 'event'
|
|
973
1096
|
});
|
|
@@ -984,8 +1107,8 @@ class MeoCordApp {
|
|
|
984
1107
|
/**
|
|
985
1108
|
* Says, once per stage, which global guards and interceptors declare no `types` and so also run
|
|
986
1109
|
* before `@On` and `@Once` handlers, when the app has any.
|
|
987
|
-
*/ noteGlobalStagesOnEvents() {
|
|
988
|
-
if (
|
|
1110
|
+
*/ noteGlobalStagesOnEvents(sources) {
|
|
1111
|
+
if (sources.length === 0) return;
|
|
989
1112
|
const { guards, interceptors } = providers.globalStagesOf(this.container);
|
|
990
1113
|
for (const [label, decorator, entries] of [
|
|
991
1114
|
[
|
|
@@ -1008,29 +1131,29 @@ class MeoCordApp {
|
|
|
1008
1131
|
/**
|
|
1009
1132
|
* Warns about handlers whose events the client options will not deliver, once per missing intent or partial. Only a
|
|
1010
1133
|
* controller's message and reaction handlers are counted, as only those run.
|
|
1011
|
-
*/ warnAboutMissingRequirements() {
|
|
1134
|
+
*/ warnAboutMissingRequirements(sources) {
|
|
1012
1135
|
const options = this.bot.options;
|
|
1013
1136
|
if (!options?.intents) return;
|
|
1014
|
-
const controllers = new Set(this.controllerClasses);
|
|
1015
1137
|
const handlers = [];
|
|
1016
|
-
for (const
|
|
1017
|
-
const
|
|
1018
|
-
for (const { event, method, once } of providers.getEventHandlers(prototype)){
|
|
1138
|
+
for (const { cls, handlers: events } of sources){
|
|
1139
|
+
for (const { event, method, once } of events){
|
|
1019
1140
|
handlers.push({
|
|
1020
|
-
label: `@${once ? 'Once' : 'On'}('${event}') in ${
|
|
1141
|
+
label: `@${once ? 'Once' : 'On'}('${event}') in ${cls.name}.${method}`,
|
|
1021
1142
|
requirements: eventRequirements(event)
|
|
1022
1143
|
});
|
|
1023
1144
|
}
|
|
1024
|
-
|
|
1145
|
+
}
|
|
1146
|
+
for (const controller of this.controllerClasses){
|
|
1147
|
+
const prototype = controller.prototype;
|
|
1025
1148
|
for (const handler of controller_decorator.getMessageHandlers(prototype)){
|
|
1026
1149
|
handlers.push({
|
|
1027
|
-
label: `${
|
|
1150
|
+
label: `${eventSources.messageDecorator(handler.pattern)} in ${controller.name}.${handler.method}`,
|
|
1028
1151
|
requirements: messageHandlerRequirements(handler, this.messageOptions ?? {})
|
|
1029
1152
|
});
|
|
1030
1153
|
}
|
|
1031
1154
|
for (const { emoji, method } of controller_decorator.getReactionHandlers(prototype)){
|
|
1032
1155
|
handlers.push({
|
|
1033
|
-
label: `${
|
|
1156
|
+
label: `${eventSources.reactionDecorator(emoji)} in ${controller.name}.${method}`,
|
|
1034
1157
|
requirements: REACTION_HANDLER_REQUIREMENTS
|
|
1035
1158
|
});
|
|
1036
1159
|
}
|
|
@@ -1042,11 +1165,9 @@ class MeoCordApp {
|
|
|
1042
1165
|
* at a time, in dependency order. A failure is logged and the next one still runs, with a warning for each hook
|
|
1043
1166
|
* whose dependencies failed. Once shutdown begins, no further hook starts.
|
|
1044
1167
|
*/ async runReadyHooks(client) {
|
|
1045
|
-
|
|
1046
|
-
this.lifecycleEntries = entries;
|
|
1047
|
-
await this.readyPass.run(()=>startupRoots.runReadyHooks(this.container, this.lifecycleUnits, client, {
|
|
1168
|
+
await this.readyPass.run(()=>eventSources.runReadyHooks(this.container, this.lifecycleUnits, client, {
|
|
1048
1169
|
primary: client.shard ? client.shard.ids.includes(0) : true
|
|
1049
|
-
},
|
|
1170
|
+
}, {
|
|
1050
1171
|
resolveFailed: (unit, error)=>this.logger.error(`Could not resolve ${unit.name} to run its lifecycle hooks:`, error),
|
|
1051
1172
|
hookFailed: (unit, error)=>this.logger.error(`onReady failed in ${unit.name}:`, error),
|
|
1052
1173
|
dependsOnFailed: (unit, failed)=>this.logger.warn(`Running onReady in ${unit.name} although it depends on ${failed.map(({ name })=>name).join(', ')}, which failed.`),
|
|
@@ -1055,15 +1176,37 @@ class MeoCordApp {
|
|
|
1055
1176
|
{
|
|
1056
1177
|
stopped: ()=>this.closing,
|
|
1057
1178
|
slowAfterMs: SLOW_READY_HOOK_MS,
|
|
1058
|
-
settled: (unit)=>
|
|
1179
|
+
settled: (unit)=>{
|
|
1180
|
+
this.readySettled.add(unit.token);
|
|
1181
|
+
if (unit.cooldownStore) this.storeReady?.resolve();
|
|
1182
|
+
},
|
|
1059
1183
|
runOnReady: this.readyPass.runOnReady
|
|
1060
1184
|
}));
|
|
1061
1185
|
// Also when shutdown stopped the hooks before the store's, so no call waits on it
|
|
1062
1186
|
this.storeReady?.resolve();
|
|
1063
1187
|
}
|
|
1064
|
-
/**
|
|
1065
|
-
|
|
1066
|
-
|
|
1188
|
+
/**
|
|
1189
|
+
* The units a shutdown closes: each one the container constructed whose `onReady`, and that of every unit it depends
|
|
1190
|
+
* on, has settled or doesn't exist, in dependency order. A unit whose `onReady` never ran gets no `onShutdown`, so a
|
|
1191
|
+
* hook that undoes `onReady` never runs without it, and neither does anything that depends on it.
|
|
1192
|
+
*/ shutdownEntries() {
|
|
1193
|
+
const units = new Set(this.lifecycleUnits.map(({ token })=>token));
|
|
1194
|
+
const closes = new Set();
|
|
1195
|
+
const entries = [];
|
|
1196
|
+
for (const unit of this.lifecycleUnits){
|
|
1197
|
+
if (!this.attempt?.constructed.has(unit.token)) continue;
|
|
1198
|
+
// Already made, so this returns the instance; a provided value may be anything, null included
|
|
1199
|
+
const instance = this.container.get(unit.token) ?? {};
|
|
1200
|
+
const ready = typeof instance.onReady !== 'function' || this.readySettled.has(unit.token);
|
|
1201
|
+
if (!ready || unit.dependencies.some((dependency)=>units.has(dependency) && !closes.has(dependency))) continue;
|
|
1202
|
+
closes.add(unit.token);
|
|
1203
|
+
entries.push(eventSources.lifecycleEntry(unit, instance));
|
|
1204
|
+
}
|
|
1205
|
+
return entries;
|
|
1206
|
+
}
|
|
1207
|
+
/** Runs the `onShutdown` hooks through the shutdown sequence, within `timeoutMs`: the stop's share of `shutdownTimeout`. */ async runShutdownHooks(timeoutMs) {
|
|
1208
|
+
// Read once the onReady in progress settled
|
|
1209
|
+
await eventSources.runShutdownSequence(this.container, ()=>this.shutdownEntries(), {
|
|
1067
1210
|
// No new call starts once the listeners are gone; an error the client emits meanwhile still has its listeners
|
|
1068
1211
|
drainCalls: ()=>{
|
|
1069
1212
|
const emitter = this.bot;
|
|
@@ -1073,7 +1216,7 @@ class MeoCordApp {
|
|
|
1073
1216
|
drainAlways: true,
|
|
1074
1217
|
runningCalls: ()=>this.calls.running(),
|
|
1075
1218
|
readyPass: this.readyPass,
|
|
1076
|
-
timeoutMs
|
|
1219
|
+
timeoutMs,
|
|
1077
1220
|
hookFailed: (name, error)=>this.logger.error(`onShutdown failed in ${name}:`, error),
|
|
1078
1221
|
warn: (message)=>this.logger.warn(message)
|
|
1079
1222
|
});
|
|
@@ -1081,7 +1224,8 @@ class MeoCordApp {
|
|
|
1081
1224
|
/**
|
|
1082
1225
|
* Runs the shutdown hooks, if the ready hooks ran, then destroys the client; a stop during login ends the login.
|
|
1083
1226
|
*
|
|
1084
|
-
* @returns Whether the client closed cleanly, which an ended login counts as
|
|
1227
|
+
* @returns Whether the client closed cleanly, which an ended login counts as, and the startup it waited for, if any,
|
|
1228
|
+
* did not fail.
|
|
1085
1229
|
*/ async closeClient() {
|
|
1086
1230
|
this.closing = true;
|
|
1087
1231
|
try {
|
|
@@ -1094,80 +1238,98 @@ class MeoCordApp {
|
|
|
1094
1238
|
/**
|
|
1095
1239
|
* Waits for the startup under way to stop making providers and services, at most `shutdownTimeout`; past it, warns
|
|
1096
1240
|
* with the factory it still awaits, which then makes nothing more.
|
|
1241
|
+
*
|
|
1242
|
+
* @returns Whether the startup failed meanwhile, as a provider's factory that rejects does, rather than stopping.
|
|
1097
1243
|
*/ async waitForStartup(preparing) {
|
|
1098
1244
|
let timer;
|
|
1099
1245
|
const outlasted = new Promise((resolve)=>{
|
|
1100
|
-
timer = setTimeout(()=>resolve(
|
|
1246
|
+
timer = setTimeout(()=>resolve('outlasted'), this.shutdownTimeout);
|
|
1101
1247
|
});
|
|
1102
|
-
const settled = preparing.then(()=>
|
|
1103
|
-
|
|
1248
|
+
const settled = preparing.then(()=>'stopped', (error)=>error instanceof Error && stops.has(error) ? 'stopped' : 'failed');
|
|
1249
|
+
const outcome = await Promise.race([
|
|
1104
1250
|
settled,
|
|
1105
1251
|
outlasted
|
|
1106
|
-
])
|
|
1252
|
+
]);
|
|
1253
|
+
clearTimeout(timer);
|
|
1254
|
+
if (outcome === 'outlasted') {
|
|
1107
1255
|
const what = this.pendingProvider ? `the factory providing ${this.pendingProvider}` : 'its providers';
|
|
1108
1256
|
this.logger.warn(`The startup still waits for ${what} after ${this.shutdownTimeout} ms; shutting down without it.`);
|
|
1109
1257
|
}
|
|
1110
|
-
|
|
1258
|
+
return outcome === 'failed';
|
|
1111
1259
|
}
|
|
1112
1260
|
async shutDownClient() {
|
|
1113
1261
|
this.logger.log('Shutting down bot...');
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1116
|
-
|
|
1262
|
+
// A failed start's shutdown under way finishes first, so its hooks run in full, before those of what it left open.
|
|
1263
|
+
// `shutdownTimeout` bounds the whole stop, as a shard's manager waits only that long, so the rest gets what is left
|
|
1264
|
+
let timeoutMs = this.shutdownTimeout;
|
|
1265
|
+
if (this.undoing) {
|
|
1266
|
+
const waitedFrom = Date.now();
|
|
1267
|
+
await this.undoing;
|
|
1268
|
+
timeoutMs = Math.max(0, timeoutMs - (Date.now() - waitedFrom));
|
|
1269
|
+
}
|
|
1270
|
+
// So nothing is made after the bot has shut down; a factory that fails meanwhile is a failure of the stop too
|
|
1271
|
+
const startupFailed = this.preparing !== undefined && await this.waitForStartup(this.preparing);
|
|
1272
|
+
// Nothing came online, so no onReady ran; the listeners go first, so none of them runs, then what the start made
|
|
1273
|
+
// without an onReady to wait for shuts down
|
|
1117
1274
|
if (this.abortLogin) {
|
|
1118
1275
|
this.logger.log('The bot was still logging in, so it stops without coming online');
|
|
1119
1276
|
this.bot.removeAllListeners();
|
|
1120
1277
|
this.abortLogin();
|
|
1278
|
+
await this.runShutdownHooks(timeoutMs);
|
|
1121
1279
|
this.logger.log('Bot has shut down');
|
|
1122
1280
|
return true;
|
|
1123
1281
|
}
|
|
1124
1282
|
if (this.activityInterval) clearInterval(this.activityInterval);
|
|
1125
1283
|
// The calls drain on every shutdown, and the onReady in progress settles first
|
|
1126
|
-
await this.runShutdownHooks();
|
|
1284
|
+
await this.runShutdownHooks(timeoutMs);
|
|
1127
1285
|
try {
|
|
1128
1286
|
this.bot.removeAllListeners();
|
|
1129
1287
|
await this.bot.destroy();
|
|
1130
1288
|
this.logger.log('Bot has shut down');
|
|
1131
|
-
return
|
|
1289
|
+
return !startupFailed;
|
|
1132
1290
|
} catch (error) {
|
|
1133
1291
|
this.logger.error('Error during shutdown:', error);
|
|
1134
1292
|
return false;
|
|
1135
1293
|
}
|
|
1136
1294
|
}
|
|
1137
|
-
constructor(controllerClasses, container, discordClient, discordToken, activities,
|
|
1295
|
+
constructor(controllerClasses, container, discordClient, discordToken, activities, /** The instances whose `@On` and `@Once` handlers run, read once the providers are made. */ eventSources$1 = ()=>[], shutdownTimeout, startup, lifecycleUnits, messageOptions = {}, warnUnanswered = false, /** Whether a manager spawned this process, as `MeoCordFactory.create` decided before anything could change it. */ shard = false, /** What each start makes; without one, as an app built by hand in a spec, nothing is shut down before ready. */ attempt){
|
|
1138
1296
|
this.controllerClasses = controllerClasses;
|
|
1139
1297
|
this.container = container;
|
|
1140
1298
|
this.discordClient = discordClient;
|
|
1141
1299
|
this.discordToken = discordToken;
|
|
1142
1300
|
this.activities = activities;
|
|
1143
|
-
this.
|
|
1301
|
+
this.eventSources = eventSources$1;
|
|
1144
1302
|
this.startup = startup;
|
|
1145
1303
|
this.messageOptions = messageOptions;
|
|
1146
1304
|
this.shard = shard;
|
|
1305
|
+
this.attempt = attempt;
|
|
1147
1306
|
this.logger = new controller_decorator.Logger(MeoCordApp.name);
|
|
1148
|
-
this.fallback =
|
|
1307
|
+
this.fallback = eventSources.createFallback(this.logger, ()=>this.messageOptions, {
|
|
1149
1308
|
cooldownNotice: (refusal)=>providers.claimCooldownNotice(this.container, refusal),
|
|
1150
1309
|
storeDownNotice: (who)=>providers.claimStoreDownNotice(this.container, who)
|
|
1151
1310
|
});
|
|
1152
1311
|
this.activityInterval = null;
|
|
1153
1312
|
/** Where the activities' rotation is: the index of the next one shown. */ this.nextActivity = 0;
|
|
1154
1313
|
/** Whether shutdown has begun, so the ready hooks start no more. */ this.closing = false;
|
|
1314
|
+
/** The tokens of the units whose `onReady` the ready pass has settled, or found they have none. */ this.readySettled = new Set();
|
|
1155
1315
|
/**
|
|
1156
1316
|
* Closes the app once, however many stops and signals ask: whether the client was destroyed cleanly. The app stays
|
|
1157
1317
|
* among the running ones until the close settles, so a signal or a manager's request meanwhile waits for it.
|
|
1158
1318
|
*/ this.close = ()=>this.stopped ?? (this.stopped = this.closeClient().finally(()=>runningApps.delete(this.close)));
|
|
1159
|
-
/** The calls under way, which shutdown waits for, less one that asked the app to stop. */ this.calls = new
|
|
1160
|
-
/** The ready hooks' pass, whose `onReady` in progress shutdown waits for, less one that asked the app to stop. */ this.readyPass = new
|
|
1161
|
-
/** Whether
|
|
1319
|
+
/** The calls under way, which shutdown waits for, less one that asked the app to stop. */ this.calls = new eventSources.RunningCalls();
|
|
1320
|
+
/** The ready hooks' pass, whose `onReady` in progress shutdown waits for, less one that asked the app to stop. */ this.readyPass = new eventSources.ReadyPass();
|
|
1321
|
+
/** Whether a start has marked where it began and not been undone, so a failed one drops what it made exactly once. */ this.begun = false;
|
|
1322
|
+
/** Whether this start's providers and services are made; a failed start drops them, so a retry makes them again. */ this.made = false;
|
|
1323
|
+
/** Whether the Discord event handlers are attached, once for the app: they resolve what they run as each event comes. */ this.attached = false;
|
|
1162
1324
|
/** Whether the bot logged in, after which `start()` has nothing to do. */ this.online = false;
|
|
1163
1325
|
/** Whether a login of this app failed, so a retry restores the client discord.js destroyed. */ this.loginFailed = false;
|
|
1164
1326
|
// First, so nothing logged from here on, before login or after, prints it
|
|
1165
1327
|
controller_decorator.hideInLogs(discordToken);
|
|
1166
|
-
this.lifecycleUnits = lifecycleUnits ??
|
|
1328
|
+
this.lifecycleUnits = lifecycleUnits ?? [];
|
|
1167
1329
|
this.bot = this.discordClient;
|
|
1168
1330
|
// Built now, so a pattern that cannot be read or two that match the same messages or customIds stop the bot as it
|
|
1169
1331
|
// is created, before start() attaches anything
|
|
1170
|
-
this.dispatcher = new
|
|
1332
|
+
this.dispatcher = new eventSources.Dispatcher({
|
|
1171
1333
|
container,
|
|
1172
1334
|
controllerClasses,
|
|
1173
1335
|
messageOptions,
|
|
@@ -1178,11 +1340,9 @@ class MeoCordApp {
|
|
|
1178
1340
|
});
|
|
1179
1341
|
this.dispatcher.getComponentRoutes();
|
|
1180
1342
|
// Read through the config's own check: a bot started without the CLI loads its config unchecked
|
|
1181
|
-
this.shutdownTimeout =
|
|
1343
|
+
this.shutdownTimeout = eventSources.shutdownTimeoutOf(shutdownTimeout, (message)=>this.logger.warn(message));
|
|
1182
1344
|
}
|
|
1183
1345
|
}
|
|
1184
|
-
/** Whether a failed login set the process exit code, so a later successful one knows to clear it. */ MeoCordApp.failedLoginSetExitCode = false;
|
|
1185
|
-
/** Whether `meocord start --dev` was told the bot could not log in, so a later login tells it the bot is online. */ MeoCordApp.toldDevRunnerLoginFailed = false;
|
|
1186
1346
|
|
|
1187
1347
|
/** The mode whose .env files Bun reads for a `NODE_ENV`: `production` or `test` as given, else `development`. */ const envMode = (nodeEnv)=>nodeEnv === 'production' || nodeEnv === 'test' ? nodeEnv : 'development';
|
|
1188
1348
|
/**
|
|
@@ -1214,16 +1374,11 @@ class MeoCordApp {
|
|
|
1214
1374
|
const productionFiles = envFiles('production');
|
|
1215
1375
|
// In Bun's order, a later file winning, as the config's dotenv also settles them
|
|
1216
1376
|
const production = Object.assign({}, ...productionFiles.map((file)=>read(root, file)));
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
for (const [key, value] of Object.entries(read(root, file)))development.set(key, {
|
|
1220
|
-
value,
|
|
1221
|
-
file
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1377
|
+
const files = envFiles(env.NODE_ENV);
|
|
1378
|
+
// Listed file by file, in Bun's order
|
|
1224
1379
|
const loaded = [
|
|
1225
|
-
...
|
|
1226
|
-
].filter(([key, { value }])
|
|
1380
|
+
...bunLoadedValues(env, root, files)
|
|
1381
|
+
].filter(([key, { value, file }])=>!productionFiles.includes(file) && env[key] === value && production[key] !== value).sort(([, a], [, b])=>files.indexOf(a.file) - files.indexOf(b.file));
|
|
1227
1382
|
if (loaded.length === 0) return none;
|
|
1228
1383
|
return {
|
|
1229
1384
|
files: [
|
|
@@ -1237,6 +1392,68 @@ class MeoCordApp {
|
|
|
1237
1392
|
const mode = envMode(nodeEnv);
|
|
1238
1393
|
return `Bun loaded ${controller_decorator.listed(files)} because NODE_ENV is ${nodeEnv || 'unset'}, and this is a production build, so ` + `${controller_decorator.listed(keys)} ${keys.length === 1 ? `has its ${mode} value` : `have their ${mode} values`}; set ` + 'NODE_ENV=production, or start with `bun --no-env-file`.';
|
|
1239
1394
|
}
|
|
1395
|
+
/**
|
|
1396
|
+
* The value Bun gives each variable the .env files set, and the file it comes from: Bun loads them before any code
|
|
1397
|
+
* runs, the file that wins in `files` first, sets only a variable not already set, and expands `$NAME`, `${NAME}` and
|
|
1398
|
+
* `${NAME:-default}` line by line from what is set so far, a later line of the same file giving its text as written.
|
|
1399
|
+
* A variable the shell set keeps the shell's value, which `env` holds, so it differs from the one given here.
|
|
1400
|
+
*/ function bunLoadedValues(env, root, files) {
|
|
1401
|
+
// Each variable's first line in Bun's order: the one Bun loads it from
|
|
1402
|
+
const lines = [];
|
|
1403
|
+
const first = new Map();
|
|
1404
|
+
for (const file of [
|
|
1405
|
+
...files
|
|
1406
|
+
].reverse()){
|
|
1407
|
+
const own = read(root, file);
|
|
1408
|
+
for (const [key, text] of Object.entries(own)){
|
|
1409
|
+
if (!first.has(key)) first.set(key, lines.length);
|
|
1410
|
+
lines.push({
|
|
1411
|
+
key,
|
|
1412
|
+
text,
|
|
1413
|
+
file,
|
|
1414
|
+
own
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const values = new Map();
|
|
1419
|
+
const fromShell = new Map();
|
|
1420
|
+
// A variable the shell set holds a value its line doesn't give; one its own line refers to back counts as from it
|
|
1421
|
+
const shellSet = (key)=>{
|
|
1422
|
+
if (!fromShell.has(key)) {
|
|
1423
|
+
fromShell.set(key, false);
|
|
1424
|
+
fromShell.set(key, env[key] !== undefined && env[key] !== valueAt(first.get(key)));
|
|
1425
|
+
}
|
|
1426
|
+
return fromShell.get(key);
|
|
1427
|
+
};
|
|
1428
|
+
const valueAt = (index)=>{
|
|
1429
|
+
if (!values.has(index)) {
|
|
1430
|
+
const { text, file, own } = lines[index];
|
|
1431
|
+
const lookup = (name)=>{
|
|
1432
|
+
const at = first.get(name);
|
|
1433
|
+
if (at === undefined || shellSet(name) || at < index) return env[name] ?? '';
|
|
1434
|
+
// Not loaded yet: its text as written in this file, or unset until a later file loads it
|
|
1435
|
+
return lines[at].file === file ? own[name] : '';
|
|
1436
|
+
};
|
|
1437
|
+
values.set(index, expandAsBun(text, lookup));
|
|
1438
|
+
}
|
|
1439
|
+
return values.get(index);
|
|
1440
|
+
};
|
|
1441
|
+
return new Map([
|
|
1442
|
+
...first
|
|
1443
|
+
].map(([key, index])=>[
|
|
1444
|
+
key,
|
|
1445
|
+
{
|
|
1446
|
+
value: valueAt(index),
|
|
1447
|
+
file: lines[index].file
|
|
1448
|
+
}
|
|
1449
|
+
]));
|
|
1450
|
+
}
|
|
1451
|
+
/** A `$NAME` or `${NAME}` reference, with `${NAME:-default}`'s default, or a `\$` Bun reads as a dollar sign. */ const BUN_REFERENCE = /\\\$|\$\{([A-Za-z0-9_]+)(?::-([^}]*))?\}|\$([A-Za-z0-9_]+)/g;
|
|
1452
|
+
/** A .env value as Bun expands it, each reference read through `lookup`; any other `$` stays as it is. */ const expandAsBun = (text, lookup)=>text.replace(BUN_REFERENCE, (match, braced, fallback, bare)=>{
|
|
1453
|
+
if (match === '\\$') return '$';
|
|
1454
|
+
const value = lookup(braced ?? bare);
|
|
1455
|
+
return fallback !== undefined && value === '' ? fallback : value;
|
|
1456
|
+
});
|
|
1240
1457
|
/** A .env file's values, or none when it is not there to read. */ function read(root, file) {
|
|
1241
1458
|
let text;
|
|
1242
1459
|
try {
|
|
@@ -1279,7 +1496,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1279
1496
|
return this.exit(1);
|
|
1280
1497
|
}
|
|
1281
1498
|
this.logger.log('Starting shards in separate processes...');
|
|
1282
|
-
|
|
1499
|
+
const registered = await this.register();
|
|
1500
|
+
// A stop that came while it waited ends the start here: nothing after it asks Discord, spawns, listens or exits
|
|
1501
|
+
if (this.stopping) return;
|
|
1502
|
+
if (!registered) return this.exitForLogin();
|
|
1283
1503
|
process.on('SIGINT', ()=>void this.stopAndExit());
|
|
1284
1504
|
process.on('SIGTERM', ()=>void this.stopAndExit());
|
|
1285
1505
|
// How `meocord start --dev` stops the bot to restart it; no stop request of the user's, whose first Ctrl+C joins it
|
|
@@ -1289,6 +1509,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1289
1509
|
const shards = config.sharding?.shards ?? 'auto';
|
|
1290
1510
|
total = shards === 'auto' ? await (this.options.recommendedShardCount ?? discord_js.fetchRecommendedShardCount)(token) : shards;
|
|
1291
1511
|
} catch (error) {
|
|
1512
|
+
if (this.stopping) return;
|
|
1292
1513
|
if (isRefusedToken(error)) {
|
|
1293
1514
|
this.logger.error(tokenMessage(token));
|
|
1294
1515
|
this.logger.debug('Asking for the shard count failed:', error);
|
|
@@ -1297,6 +1518,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1297
1518
|
}
|
|
1298
1519
|
return this.exitForLogin();
|
|
1299
1520
|
}
|
|
1521
|
+
if (this.stopping) return;
|
|
1300
1522
|
const manager = (this.options.createManager ?? ((path, opts)=>new discord_js.ShardingManager(path, opts)))(file, {
|
|
1301
1523
|
token,
|
|
1302
1524
|
totalShards: total,
|
|
@@ -1330,13 +1552,15 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1330
1552
|
try {
|
|
1331
1553
|
const rest = this.options.createRest?.(token) ?? new discord_js.REST().setToken(token);
|
|
1332
1554
|
const applicationId = (await rest.get(discord_js.Routes.currentApplication())).id;
|
|
1555
|
+
// Stopped while it asked: the commands are left as they are
|
|
1556
|
+
if (this.stopping) return true;
|
|
1333
1557
|
await providers.registerCommands({
|
|
1334
1558
|
rest,
|
|
1335
1559
|
applicationId,
|
|
1336
1560
|
controllerClasses,
|
|
1337
1561
|
logger: this.logger,
|
|
1338
1562
|
config: config.commands,
|
|
1339
|
-
development:
|
|
1563
|
+
development: registersAsDevelopment(),
|
|
1340
1564
|
force: process.env[FORCE_REGISTER_ENV] === '1'
|
|
1341
1565
|
});
|
|
1342
1566
|
} catch (error) {
|
|
@@ -1378,7 +1602,8 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1378
1602
|
if (this.now() - state.spawnedAt >= RESPAWN_RESET_MS) state.attempts = 0;
|
|
1379
1603
|
const delay = Math.min(RESPAWN_BASE_MS * 2 ** state.attempts, RESPAWN_CAP_MS);
|
|
1380
1604
|
state.attempts++;
|
|
1381
|
-
|
|
1605
|
+
const how = child?.signalCode ? `on ${child.signalCode}` : `with code ${child?.exitCode ?? 'unknown'}`;
|
|
1606
|
+
this.logger.warn(`Shard ${shard.id} exited ${how}; restarting it in ${delay} ms.`);
|
|
1382
1607
|
state.timer = setTimeout(()=>{
|
|
1383
1608
|
state.timer = undefined;
|
|
1384
1609
|
if (!this.stopping && !this.fatal) void this.spawn(shard);
|
|
@@ -1451,7 +1676,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1451
1676
|
if (shard.process) shard.kill();
|
|
1452
1677
|
});
|
|
1453
1678
|
}
|
|
1454
|
-
const wait = this.shutdownTimeout +
|
|
1679
|
+
const wait = this.shutdownTimeout + eventSources.SHUTDOWN_MARGIN_MS;
|
|
1455
1680
|
let timer;
|
|
1456
1681
|
const timedOut = new Promise((resolve)=>{
|
|
1457
1682
|
timer = setTimeout(()=>resolve('timeout'), wait);
|
|
@@ -1492,7 +1717,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1492
1717
|
this.sleep = options.sleep ?? sleep;
|
|
1493
1718
|
this.now = options.now ?? (()=>Date.now());
|
|
1494
1719
|
this.stopRequest = stopRequests(this.now);
|
|
1495
|
-
this.shutdownTimeout =
|
|
1720
|
+
this.shutdownTimeout = eventSources.shutdownTimeoutOf(options.config.shutdownTimeout, (message)=>this.logger.warn(message));
|
|
1496
1721
|
}
|
|
1497
1722
|
}
|
|
1498
1723
|
|
|
@@ -1507,8 +1732,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1507
1732
|
container.bind(cls).toSelf().inSingletonScope();
|
|
1508
1733
|
// By constructor type or @inject token; an interface-typed parameter records Object, which is skipped
|
|
1509
1734
|
for (const dep of providers.injectedTokens(cls)){
|
|
1510
|
-
if (
|
|
1511
|
-
else if (providers.isAppClassToken(dep)) bindDependencies(container, dep, providers$1);
|
|
1735
|
+
if (providers.isAppClassToken(dep)) bindDependencies(container, dep, providers$1);
|
|
1512
1736
|
}
|
|
1513
1737
|
}
|
|
1514
1738
|
/**
|
|
@@ -1597,6 +1821,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1597
1821
|
controller_decorator.markExplained(error);
|
|
1598
1822
|
}
|
|
1599
1823
|
}
|
|
1824
|
+
// A built bot that refuses its app exits 1, as one whose start fails does, whatever main.ts does with the error
|
|
1825
|
+
if (!shard && controller_decorator.isBuiltApplication() && controller_decorator.isRefusal(error) && (process.exitCode === undefined || process.exitCode === 0)) {
|
|
1826
|
+
process.exitCode = 1;
|
|
1827
|
+
}
|
|
1600
1828
|
throw error;
|
|
1601
1829
|
}
|
|
1602
1830
|
}
|
|
@@ -1627,31 +1855,31 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1627
1855
|
// error each check finds is reported, the first thrown
|
|
1628
1856
|
const providers$1 = controller_decorator.collectStartupErrors(()=>{
|
|
1629
1857
|
declared.forEach(controller_decorator.startupError);
|
|
1630
|
-
|
|
1858
|
+
eventSources.assertDistinctCommands(options.controllers);
|
|
1631
1859
|
const providers$1 = providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
|
|
1632
1860
|
providers.assertProvidedServices(typeof target === 'function' ? target.name : String(target), options.services ?? [], providers$1, '@MeoCord({ providers })');
|
|
1633
1861
|
return providers$1;
|
|
1634
1862
|
});
|
|
1635
1863
|
// A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
|
|
1636
1864
|
const themeResolver = controller_decorator.themeResolverClass(options.themeFor);
|
|
1637
|
-
//
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1865
|
+
// Every class the app runs, its stages and presenter and what they inject included
|
|
1866
|
+
const stages = providers.appStages(target);
|
|
1867
|
+
const runs = eventSources.startupClasses({
|
|
1868
|
+
controllers: options.controllers,
|
|
1869
|
+
services: options.services ?? [],
|
|
1870
|
+
providers: providers$1,
|
|
1871
|
+
stages,
|
|
1872
|
+
presenter: options.presenter,
|
|
1873
|
+
cooldownStore: options.cooldownStore,
|
|
1874
|
+
themeFor: options.themeFor,
|
|
1875
|
+
observers: providers.appObservers(target)
|
|
1876
|
+
});
|
|
1649
1877
|
// A shard's manager runs the same checks, so a sharded bot warns once
|
|
1650
1878
|
if (!shard) {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1879
|
+
eventSources.warnUnregisteredCommands(options.controllers);
|
|
1880
|
+
eventSources.warnInheritedRoutes(options.controllers);
|
|
1881
|
+
eventSources.warnOverlappingPatterns(options.controllers);
|
|
1882
|
+
eventSources.warnHandlersOffControllers(options.controllers, runs.classes);
|
|
1655
1883
|
const developmentEnv = bunDevelopmentValues();
|
|
1656
1884
|
if (developmentEnv.keys.length > 0) {
|
|
1657
1885
|
this.logger.warn(bunDevelopmentWarning(developmentEnv));
|
|
@@ -1672,20 +1900,9 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1672
1900
|
}
|
|
1673
1901
|
// The checks of what the app binds and runs report every error each finds, and the first is thrown
|
|
1674
1902
|
return controller_decorator.collectStartupErrors(()=>{
|
|
1675
|
-
// Every class the app runs
|
|
1676
|
-
//
|
|
1677
|
-
|
|
1678
|
-
const runs = startupRoots.startupClasses({
|
|
1679
|
-
controllers: options.controllers,
|
|
1680
|
-
services: options.services ?? [],
|
|
1681
|
-
providers: providers$1,
|
|
1682
|
-
stages,
|
|
1683
|
-
presenter: options.presenter,
|
|
1684
|
-
cooldownStore: options.cooldownStore,
|
|
1685
|
-
themeFor: options.themeFor,
|
|
1686
|
-
observers: providers.appObservers(target)
|
|
1687
|
-
});
|
|
1688
|
-
startupRoots.assertStartupClasses(runs, {
|
|
1903
|
+
// Every class the app runs checked before binding, where inversify would otherwise fail first with an error about
|
|
1904
|
+
// compiler options, or a guard only at its first call
|
|
1905
|
+
eventSources.assertStartupClasses(runs, {
|
|
1689
1906
|
translator: options.i18n !== undefined || providers$1.has(controller_decorator.Translator)
|
|
1690
1907
|
});
|
|
1691
1908
|
// Outside a built application, the kept errors of the stages and other classes the app's classes reach
|
|
@@ -1698,7 +1915,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1698
1915
|
providers.markStartupChecked(container, [
|
|
1699
1916
|
...runs.classes,
|
|
1700
1917
|
...providers.meocordClasses()
|
|
1701
|
-
],
|
|
1918
|
+
], eventSources.lateGuardCheck(providers$1, translator));
|
|
1702
1919
|
providers.bindGlobalStages(container, stages);
|
|
1703
1920
|
// Bound now, so no provider stands in for it; made once the checks pass, the one Client the app logs in with
|
|
1704
1921
|
const clientOptions = shardedCooldownStore.clientOptionsWithSharding(this.effectiveConfig(meocordConfig, shard), options.clientOptions);
|
|
@@ -1709,7 +1926,8 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1709
1926
|
}
|
|
1710
1927
|
// Bound before the app's classes, so a class that injects it gets this instance; filled once they are bound
|
|
1711
1928
|
const appClasses = [];
|
|
1712
|
-
|
|
1929
|
+
const registry = new providers.HandlerRegistry(appClasses, options.messages, ()=>options.i18n, options.controllers);
|
|
1930
|
+
container.bind(providers.HandlerRegistry).toConstantValue(registry);
|
|
1713
1931
|
container.bind(providers.ShardContext).toDynamicValue(()=>{
|
|
1714
1932
|
const client = container.get(discord_js.Client);
|
|
1715
1933
|
return new providers.ShardContext(client, (service, method, args)=>Reflect.get(client, providers.SHARD_CALL_KEY)(service, method, args));
|
|
@@ -1729,12 +1947,9 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1729
1947
|
// After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
|
|
1730
1948
|
// a class token that is provided is not also bound as itself
|
|
1731
1949
|
for (const [token, provider] of providers$1){
|
|
1732
|
-
|
|
1733
|
-
if (
|
|
1734
|
-
controller_decorator.
|
|
1735
|
-
}
|
|
1736
|
-
if (container.isBound(token)) {
|
|
1737
|
-
controller_decorator.startupError(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${providers.tokenName(token)}, which MeoCord binds itself.`));
|
|
1950
|
+
const refusal = providers.appProviderRefusal(`${target.name}`, token, (own)=>container.isBound(own), this.logger);
|
|
1951
|
+
if (refusal) {
|
|
1952
|
+
controller_decorator.startupError(refusal);
|
|
1738
1953
|
continue;
|
|
1739
1954
|
}
|
|
1740
1955
|
providers.bindProvider(container, provider, (cls)=>bindDependencies(container, cls, providers$1));
|
|
@@ -1751,7 +1966,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1751
1966
|
const observers = providers.appObservers(target);
|
|
1752
1967
|
for (const observer of observers)bindDependencies(container, observer, providers$1);
|
|
1753
1968
|
// What the stages and presenter inject, bound now so a service among them is made as the bot comes online
|
|
1754
|
-
const stageServices =
|
|
1969
|
+
const stageServices = eventSources.stageDependencies(runs, providers$1, options.presenter);
|
|
1755
1970
|
for (const service of stageServices)bindDependencies(container, service, providers$1);
|
|
1756
1971
|
// What follows walks what these bound, so a class refused here is not reported again as missing
|
|
1757
1972
|
controller_decorator.stopOnStartupErrors();
|
|
@@ -1819,26 +2034,35 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1819
2034
|
providers.buildMessageRoutes(options.controllers, options.messages);
|
|
1820
2035
|
// Every check has run, and touched only the container: from here the app is made, once none found an error
|
|
1821
2036
|
controller_decorator.stopOnStartupErrors();
|
|
2037
|
+
for (const warning of eventSources.perCallEventWarnings(runs, providers$1, options.presenter))this.logger.warn(warning);
|
|
1822
2038
|
const discordClient = container.get(discord_js.Client);
|
|
1823
2039
|
Reflect.set(discordClient, providers.SHARD_CALL_KEY, providers.shardCallHandler(container, ()=>callable, 'this app'));
|
|
1824
|
-
//
|
|
1825
|
-
|
|
1826
|
-
Reflect.defineMetadata(controller_decorator.META.container, container, cls);
|
|
1827
|
-
}
|
|
1828
|
-
providers.recordInstanceContainers(container, runs.classes);
|
|
2040
|
+
// Every class the app runs, so @UseGuard resolves guards on a direct call to any of them
|
|
2041
|
+
providers.prepareDirectCalls(container, runs.classes);
|
|
1829
2042
|
providers.prepareThemes(container, appClasses);
|
|
1830
2043
|
// Run by start() before it logs in: what may inject a provided value is resolved once every factory
|
|
1831
2044
|
// has made its value, including those that return a promise
|
|
1832
2045
|
const logger = this.logger;
|
|
2046
|
+
let sources;
|
|
1833
2047
|
const startup = async (control)=>{
|
|
2048
|
+
let made;
|
|
1834
2049
|
try {
|
|
1835
|
-
await providers.resolveProviders(container, providers$1, order, control);
|
|
2050
|
+
made = await providers.resolveProviders(container, providers$1, order, control);
|
|
1836
2051
|
} catch (error) {
|
|
1837
2052
|
logger.error(`${error.message}. The bot cannot start without it.`);
|
|
1838
2053
|
logger.debug('Provider failure:', error.cause);
|
|
1839
2054
|
controller_decorator.markExplained(error);
|
|
1840
2055
|
throw error;
|
|
1841
2056
|
}
|
|
2057
|
+
// A stop came while the providers were being made: what follows resolves provided values, which would make the
|
|
2058
|
+
// factories it left unmade
|
|
2059
|
+
if (!made) return;
|
|
2060
|
+
// Read once every provided value is made, and before anything the app makes could list its handlers
|
|
2061
|
+
if (!sources) {
|
|
2062
|
+
sources = eventSources.eventSources(container, providers$1, lifecycle, appClasses, (text)=>logger.warn(text));
|
|
2063
|
+
providers.listEventSources(registry, sources);
|
|
2064
|
+
providers.prepareLateThemes(container, sources.map(({ cls })=>cls));
|
|
2065
|
+
}
|
|
1842
2066
|
// The listed services are made now, so constructors that attach listeners or connect run before login; a stop
|
|
1843
2067
|
// meanwhile makes no more of them
|
|
1844
2068
|
for (const svc of options.services ?? []){
|
|
@@ -1853,12 +2077,39 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1853
2077
|
providers.registerClientTheme(discordClient, container);
|
|
1854
2078
|
controller_decorator.registerClientTranslator(discordClient, options.i18n);
|
|
1855
2079
|
};
|
|
1856
|
-
return new MeoCordApp(options.controllers, container, discordClient, meocordConfig.discordToken, options.activities,
|
|
1857
|
-
options.warnUnanswered ?? process.env.NODE_ENV === 'development', shard);
|
|
2080
|
+
return new MeoCordApp(options.controllers, container, discordClient, meocordConfig.discordToken, options.activities, ()=>sources ?? [], meocordConfig.shutdownTimeout, startup, lifecycle, options.messages, // In development only, unless the app says otherwise
|
|
2081
|
+
options.warnUnanswered ?? process.env.NODE_ENV === 'development', shard, startAttempt(container, lifecycle, providers$1));
|
|
1858
2082
|
});
|
|
1859
2083
|
}
|
|
1860
2084
|
}
|
|
1861
2085
|
MeoCordFactory.logger = new controller_decorator.Logger();
|
|
2086
|
+
/**
|
|
2087
|
+
* What the app's starts make: each lifecycle unit is recorded as the container constructs it, as the testing module
|
|
2088
|
+
* records them, and a container snapshot marks where a start begins, so a failed start drops every singleton it made,
|
|
2089
|
+
* the values factory providers keep included.
|
|
2090
|
+
*/ function startAttempt(container, lifecycle, providers$1) {
|
|
2091
|
+
const constructed = new Set();
|
|
2092
|
+
for (const { token } of lifecycle){
|
|
2093
|
+
container.onActivation(token, (_context, instance)=>{
|
|
2094
|
+
constructed.add(token);
|
|
2095
|
+
return instance;
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
const drop = ()=>{
|
|
2099
|
+
container.restore();
|
|
2100
|
+
providers.forgetFactoryValues(container);
|
|
2101
|
+
};
|
|
2102
|
+
const given = new Set(lifecycle.map(({ token })=>token).filter((token)=>{
|
|
2103
|
+
const provider = providers$1.get(token);
|
|
2104
|
+
return provider !== undefined && providers.isValueProvider(provider);
|
|
2105
|
+
}));
|
|
2106
|
+
return {
|
|
2107
|
+
constructed,
|
|
2108
|
+
given,
|
|
2109
|
+
begin: ()=>container.snapshot(),
|
|
2110
|
+
drop
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
1862
2113
|
|
|
1863
2114
|
exports.HandlerRegistry = providers.HandlerRegistry;
|
|
1864
2115
|
exports.ShardContext = providers.ShardContext;
|