meocord 4.2.0 → 4.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +322 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +188 -169
- package/dist/cjs/decorator/index.cjs +52 -7
- package/dist/cjs/testing/index.cjs +1854 -311
- package/dist/cli.json +1 -1
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +13 -3
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/errors.js +25 -0
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +89 -15
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +52 -11
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +9 -8
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +10 -4
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +59 -34
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-help.js +32 -6
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/providers.js +19 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/shard-context.js +5 -6
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +7 -2
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +31 -0
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +20 -13
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +225 -30
- package/dist/esm/testing/meocord-testing-module.js +158 -44
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1250 -182
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +40 -17
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +2 -2
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +3 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +30 -2
- package/dist/types/common/index.d.cts +10 -7
- package/dist/types/common/index.d.ts +10 -7
- package/dist/types/core/index.d.cts +3 -2
- package/dist/types/core/index.d.ts +3 -2
- package/dist/types/decorator/index.d.cts +1 -0
- package/dist/types/decorator/index.d.ts +1 -0
- package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
- package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
- package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
- package/dist/types/testing/index.d.cts +198 -62
- package/dist/types/testing/index.d.ts +198 -62
- package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +2 -2
package/dist/cjs/core/index.cjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
require('reflect-metadata');
|
|
4
|
-
var providers = require('../_shared/providers-
|
|
4
|
+
var providers = require('../_shared/providers-B4MOFBrW.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-U2NBWcoU.cjs');
|
|
8
8
|
require('node:crypto');
|
|
9
|
-
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-
|
|
10
|
-
var
|
|
11
|
-
var startupRoots = require('../_shared/startup-roots-CQo8mSP8.cjs');
|
|
9
|
+
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-HTUb3Ii_.cjs');
|
|
10
|
+
var startupRoots = require('../_shared/startup-roots-lA5xkBFq.cjs');
|
|
12
11
|
var enum_index = require('../enum/index.cjs');
|
|
13
12
|
var node_fs = require('node:fs');
|
|
14
13
|
var path = require('node:path');
|
|
15
14
|
var node_util = require('node:util');
|
|
16
|
-
var fs = require('fs');
|
|
17
15
|
require('node:async_hooks');
|
|
18
16
|
require('crypto');
|
|
17
|
+
require('fs');
|
|
18
|
+
require('path');
|
|
19
19
|
require('cli-table3');
|
|
20
20
|
require('node:url');
|
|
21
21
|
require('dayjs');
|
|
@@ -23,11 +23,10 @@ require('dayjs/plugin/utc.js');
|
|
|
23
23
|
require('dayjs/plugin/timezone.js');
|
|
24
24
|
require('module');
|
|
25
25
|
require('chalk');
|
|
26
|
-
require('../_shared/translate-error-
|
|
26
|
+
require('../_shared/translate-error-CbBHUu5P.cjs');
|
|
27
27
|
|
|
28
28
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
29
29
|
|
|
30
|
-
var path__default$1 = /*#__PURE__*/_interopDefaultCompat(path$1);
|
|
31
30
|
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
32
31
|
|
|
33
32
|
const MESSAGES = [
|
|
@@ -446,8 +445,8 @@ const slots = globalThis;
|
|
|
446
445
|
* on Windows a signal ends a process outright, skipping its `onShutdown` hooks. The bundle's pre-entry calls it first,
|
|
447
446
|
* so a stop is heard however early it comes; until an app or shard manager says how it stops, the process exits at
|
|
448
447
|
* once, as SIGTERM's default would.
|
|
449
|
-
*/ function listenForDevRunnerStop() {
|
|
450
|
-
if (!underDevRunner() || slots[LISTENING]) return;
|
|
448
|
+
*/ function listenForDevRunnerStop(shard = shardedCooldownStore.isShardProcess(process.env)) {
|
|
449
|
+
if (!underDevRunner(shard) || slots[LISTENING]) return;
|
|
451
450
|
slots[LISTENING] = true;
|
|
452
451
|
process.on('message', (message)=>{
|
|
453
452
|
if (!isDevRunnerCommand(message)) return;
|
|
@@ -458,19 +457,19 @@ const slots = globalThis;
|
|
|
458
457
|
// A 'message' listener keeps the process alive for its channel; what the bot does decides that, as without one
|
|
459
458
|
process.channel?.unref();
|
|
460
459
|
}
|
|
461
|
-
/** Has a stop from `meocord start --dev` run `stop`, as SIGINT and SIGTERM do
|
|
462
|
-
listenForDevRunnerStop();
|
|
460
|
+
/** Has a stop from `meocord start --dev` run `stop`, as SIGINT and SIGTERM do, in a process whose role is `shard`. */ function onDevRunnerStop(stop, shard) {
|
|
461
|
+
listenForDevRunnerStop(shard);
|
|
463
462
|
slots[STOP] = stop;
|
|
464
463
|
}
|
|
465
|
-
/** Whether `meocord start --dev` runs this process and listens on its channel. */ function underDevRunner() {
|
|
466
|
-
return process.env[DEV_RUNNER_ENV] === '1' && !!process.send && !
|
|
464
|
+
/** Whether `meocord start --dev` runs this process, a shard or not as `shard` says, and listens on its channel. */ function underDevRunner(shard) {
|
|
465
|
+
return process.env[DEV_RUNNER_ENV] === '1' && !!process.send && !shard;
|
|
467
466
|
}
|
|
468
467
|
/**
|
|
469
468
|
* Tells `meocord start --dev`, when it runs this process, whether the bot could log in, and waits until it is sent, or
|
|
470
469
|
* until the dev runner is taken to be gone. A caller sets the exit code before sending, so it holds however the send
|
|
471
470
|
* ends. A shard tells its manager instead, which tells the dev runner in turn.
|
|
472
|
-
*/ async function tellDevRunner(message) {
|
|
473
|
-
if (underDevRunner()) await sendToParent(message);
|
|
471
|
+
*/ async function tellDevRunner(message, shard) {
|
|
472
|
+
if (underDevRunner(shard)) await sendToParent(message);
|
|
474
473
|
}
|
|
475
474
|
|
|
476
475
|
/** The errors the gateway closes with, which discord.js passes on without a code. */ const GATEWAY_CLOSE_MESSAGES = {
|
|
@@ -638,11 +637,11 @@ let shuttingDown;
|
|
|
638
637
|
* Shuts every started app down and exits, with a code already set or 0, and 1 if a client failed to close. Signals call
|
|
639
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
|
|
640
639
|
* same request; one after it forces exit 1, except in a shard, whose manager forces it.
|
|
641
|
-
*/ async function shutdownAndExit() {
|
|
640
|
+
*/ async function shutdownAndExit(shard) {
|
|
642
641
|
const request = stopRequest();
|
|
643
642
|
if (request !== 'first') {
|
|
644
643
|
// A shard hears Ctrl+C both directly and from its manager, which owns forcing it; so it waits
|
|
645
|
-
if (request === 'duplicate' ||
|
|
644
|
+
if (request === 'duplicate' || shard) return;
|
|
646
645
|
process.exit(1);
|
|
647
646
|
return;
|
|
648
647
|
}
|
|
@@ -663,47 +662,48 @@ let shuttingDown;
|
|
|
663
662
|
controller_decorator.markExplained(error);
|
|
664
663
|
return error;
|
|
665
664
|
}
|
|
666
|
-
/** One pair of signal listeners for the process, however many apps it starts. */ function installSignalHandlers() {
|
|
665
|
+
/** One pair of signal listeners for the process, however many apps it starts, for the role the first decided. */ function installSignalHandlers(shard) {
|
|
667
666
|
if (signalHandlersInstalled) return;
|
|
668
667
|
signalHandlersInstalled = true;
|
|
669
|
-
process.on('SIGINT', ()=>void shutdownAndExit());
|
|
670
|
-
process.on('SIGTERM', ()=>void shutdownAndExit());
|
|
668
|
+
process.on('SIGINT', ()=>void shutdownAndExit(shard));
|
|
669
|
+
process.on('SIGTERM', ()=>void shutdownAndExit(shard));
|
|
671
670
|
// How `meocord start --dev` stops the application to restart it, a signal being no graceful stop on Windows. It is
|
|
672
671
|
// not the user's stop request: their first Ctrl+C during it joins this shutdown, and only a repeat forces exit 1.
|
|
673
|
-
onDevRunnerStop(()=>void shutDown());
|
|
672
|
+
onDevRunnerStop(()=>void shutDown(), shard);
|
|
674
673
|
// A shard stops when its manager asks, or when the manager is gone and cannot ask
|
|
675
|
-
if (
|
|
674
|
+
if (shard) {
|
|
676
675
|
process.on('message', (message)=>{
|
|
677
|
-
if (shardedCooldownStore.isShardMessage(message) && message.meocord === 'shutdown') void shutdownAndExit();
|
|
676
|
+
if (shardedCooldownStore.isShardMessage(message) && message.meocord === 'shutdown') void shutdownAndExit(shard);
|
|
678
677
|
});
|
|
679
|
-
process.on('disconnect', ()=>void shutdownAndExit());
|
|
678
|
+
process.on('disconnect', ()=>void shutdownAndExit(shard));
|
|
680
679
|
}
|
|
681
680
|
}
|
|
682
|
-
/** Tells the manager a shard cannot log in, and why, and waits until the message is sent. */ async function reportFatalLogin(code, reason) {
|
|
683
|
-
if (
|
|
681
|
+
/** Tells the manager a shard cannot log in, and why, and waits until the message is sent. */ async function reportFatalLogin(shard, code, reason) {
|
|
682
|
+
if (shard) await tellManager({
|
|
684
683
|
meocord: 'fatal',
|
|
685
684
|
code,
|
|
686
685
|
message: reason
|
|
687
686
|
});
|
|
688
687
|
}
|
|
688
|
+
/** How a shutdown warning names an interaction still being handled: by its command, or a component's customId. */ function interactionLabel(interaction) {
|
|
689
|
+
if ('commandName' in interaction) return `interaction "/${controller_decorator.escapeForLog(interaction.commandName)}"`;
|
|
690
|
+
if ('customId' in interaction) return `interaction "${controller_decorator.escapeForLog(interaction.customId)}"`;
|
|
691
|
+
return '"interactionCreate"';
|
|
692
|
+
}
|
|
689
693
|
class MeoCordApp {
|
|
690
694
|
/**
|
|
691
695
|
* Runs an event handler so its failure is logged against the event instead of surfacing as an
|
|
692
696
|
* unhandled rejection, which would terminate the whole bot.
|
|
693
|
-
*/ async runListener(event, run) {
|
|
694
|
-
const
|
|
697
|
+
*/ async runListener(event, run, label = `"${event}"`) {
|
|
698
|
+
const handle = async ()=>{
|
|
695
699
|
try {
|
|
696
700
|
await run();
|
|
697
701
|
} catch (error) {
|
|
698
702
|
this.logger.error(`Unhandled error while handling "${event}":`, error);
|
|
699
703
|
}
|
|
700
|
-
}
|
|
701
|
-
// A call, which
|
|
702
|
-
|
|
703
|
-
this.calls.add(handled);
|
|
704
|
-
void handled.then(()=>this.calls.delete(handled));
|
|
705
|
-
}
|
|
706
|
-
await handled;
|
|
704
|
+
};
|
|
705
|
+
// A call, which shutdown waits for before the onShutdown hooks; the ready listener's work is not one
|
|
706
|
+
await (event === 'clientReady' ? handle() : this.calls.run(label, handle));
|
|
707
707
|
}
|
|
708
708
|
/**
|
|
709
709
|
* Shows the next of the app's activities, in the order listed, starting again after the last. Guarded apart from
|
|
@@ -725,7 +725,7 @@ class MeoCordApp {
|
|
|
725
725
|
if (this.online) return;
|
|
726
726
|
this.starting ?? (this.starting = this.startOnce().catch((error)=>{
|
|
727
727
|
// 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
|
-
if (
|
|
728
|
+
if (this.shard && !this.closing) endFailedShard(error);
|
|
729
729
|
throw error;
|
|
730
730
|
}).finally(()=>this.starting = undefined));
|
|
731
731
|
return this.starting;
|
|
@@ -735,7 +735,10 @@ class MeoCordApp {
|
|
|
735
735
|
* which then asks this one too, so the process ends. A client that fails to close sets `process.exitCode` to 1,
|
|
736
736
|
* unless a code is already set.
|
|
737
737
|
*/ async stop() {
|
|
738
|
-
|
|
738
|
+
// A handler or an onReady awaiting stop() would otherwise hold up the shutdown that waits for it
|
|
739
|
+
this.calls.excuseCurrent();
|
|
740
|
+
this.readyPass.excuseCurrent();
|
|
741
|
+
if (this.shard) await tellManager({
|
|
739
742
|
meocord: 'stop'
|
|
740
743
|
});
|
|
741
744
|
// As a signal's shutdown exits 1 for a client that failed to close, a code another failure set aside
|
|
@@ -745,14 +748,20 @@ class MeoCordApp {
|
|
|
745
748
|
this.logger.log('Starting bot...');
|
|
746
749
|
// Before the providers are made, so a signal, or a shard's manager, can stop a start that is slow to come online
|
|
747
750
|
runningApps.add(this.close);
|
|
748
|
-
installSignalHandlers();
|
|
751
|
+
installSignalHandlers(this.shard);
|
|
749
752
|
// A shard whose manager is already gone has no one to answer to, and nothing would stop it later
|
|
750
|
-
if (
|
|
753
|
+
if (this.shard && shardedCooldownStore.managerGone()) void shutdownAndExit(this.shard);
|
|
751
754
|
// A bot runs one app, whose theme code outside any call reads, its providers' factories included; claimed for
|
|
752
755
|
// each start, as a failed one gives it up
|
|
753
756
|
providers.claimAmbientAppTheme(this.container);
|
|
754
757
|
if (!this.prepared) {
|
|
755
|
-
|
|
758
|
+
// Kept while it runs, so a stop meanwhile waits for it
|
|
759
|
+
this.preparing = this.prepare();
|
|
760
|
+
try {
|
|
761
|
+
await this.preparing;
|
|
762
|
+
} finally{
|
|
763
|
+
this.preparing = undefined;
|
|
764
|
+
}
|
|
756
765
|
this.prepared = true;
|
|
757
766
|
}
|
|
758
767
|
if (this.loginFailed) {
|
|
@@ -765,7 +774,10 @@ class MeoCordApp {
|
|
|
765
774
|
// Every provided value is made before anything that injects it is resolved, and before login
|
|
766
775
|
if (this.startup) {
|
|
767
776
|
try {
|
|
768
|
-
await this.startup(
|
|
777
|
+
await this.startup({
|
|
778
|
+
stopped: ()=>this.closing,
|
|
779
|
+
pending: (name)=>this.pendingProvider = name
|
|
780
|
+
});
|
|
769
781
|
} catch (error) {
|
|
770
782
|
providers.releaseAmbientAppTheme(this.container);
|
|
771
783
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
@@ -775,7 +787,8 @@ class MeoCordApp {
|
|
|
775
787
|
throw error;
|
|
776
788
|
}
|
|
777
789
|
}
|
|
778
|
-
// A stop that came while the providers were being made has
|
|
790
|
+
// A stop that came while the providers were being made has waited for them, and the startup made no more after it;
|
|
791
|
+
// nothing is attached or logged in
|
|
779
792
|
if (this.closing) throw stoppedBeforeOnline();
|
|
780
793
|
// Calls wait for the store's onReady, which may connect it, before asking it
|
|
781
794
|
if (this.lifecycleUnits.some((unit)=>unit.cooldownStore)) {
|
|
@@ -791,10 +804,10 @@ class MeoCordApp {
|
|
|
791
804
|
// Started before registration and not waited on by it, so a slow or failed registration never holds them up
|
|
792
805
|
const readyHooks = this.runReadyHooks(readyClient ?? this.bot);
|
|
793
806
|
// With process sharding, the manager registers once for every shard
|
|
794
|
-
if (!
|
|
807
|
+
if (!this.shard) await this.registerCommands();
|
|
795
808
|
await readyHooks;
|
|
796
809
|
}));
|
|
797
|
-
this.bot.on('interactionCreate', startupRoots.ownInteractionListener((interaction)=>this.runListener('interactionCreate', ()=>this.dispatcher.interaction(interaction))));
|
|
810
|
+
this.bot.on('interactionCreate', startupRoots.ownInteractionListener((interaction)=>this.runListener('interactionCreate', ()=>this.dispatcher.interaction(interaction), interactionLabel(interaction))));
|
|
798
811
|
this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.dispatcher.message(message)));
|
|
799
812
|
this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.dispatcher.reaction(reaction, {
|
|
800
813
|
user,
|
|
@@ -837,18 +850,18 @@ class MeoCordApp {
|
|
|
837
850
|
const explanation = fatal && explainLoginFailure(fatal, this.bot.options?.intents, this.discordToken);
|
|
838
851
|
if (explanation) {
|
|
839
852
|
// The explanation is what to act on, and the stack only for debugging. A shard's manager logs it instead.
|
|
840
|
-
if (!
|
|
853
|
+
if (!this.shard) this.logger.error(explanation);
|
|
841
854
|
this.logger.debug('Login failed:', error);
|
|
842
855
|
controller_decorator.markExplained(error);
|
|
843
856
|
}
|
|
844
|
-
if (fatal) await reportFatalLogin(fatal, explanation ?? (error instanceof Error ? error.message : String(error)));
|
|
857
|
+
if (fatal) await reportFatalLogin(this.shard, fatal, explanation ?? (error instanceof Error ? error.message : String(error)));
|
|
845
858
|
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
846
859
|
process.exitCode = 1;
|
|
847
860
|
MeoCordApp.failedLoginSetExitCode = true;
|
|
848
861
|
}
|
|
849
862
|
await tellDevRunner({
|
|
850
863
|
meocord: 'login-failed'
|
|
851
|
-
});
|
|
864
|
+
}, this.shard);
|
|
852
865
|
MeoCordApp.toldDevRunnerLoginFailed = true;
|
|
853
866
|
this.loginFailed = true;
|
|
854
867
|
throw error;
|
|
@@ -866,7 +879,7 @@ class MeoCordApp {
|
|
|
866
879
|
MeoCordApp.toldDevRunnerLoginFailed = false;
|
|
867
880
|
await tellDevRunner({
|
|
868
881
|
meocord: 'online'
|
|
869
|
-
});
|
|
882
|
+
}, this.shard);
|
|
870
883
|
}
|
|
871
884
|
this.online = true;
|
|
872
885
|
this.logger.log('Bot is online!');
|
|
@@ -949,18 +962,20 @@ class MeoCordApp {
|
|
|
949
962
|
} else logError(error);
|
|
950
963
|
return undefined;
|
|
951
964
|
};
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
965
|
+
const label = `@${once ? 'Once' : 'On'}('${event}') in ${lifecycleClass.name}.${method}`;
|
|
966
|
+
// A call, as a dispatched one is, which shutdown waits for
|
|
967
|
+
const listener = (...args)=>this.calls.run(label, async ()=>{
|
|
968
|
+
try {
|
|
969
|
+
const instance = this.container.get(lifecycleClass);
|
|
970
|
+
await providers.runHandler(this.container, instance, method, args, {
|
|
971
|
+
fallback,
|
|
972
|
+
type: 'event'
|
|
973
|
+
});
|
|
974
|
+
} catch (error) {
|
|
975
|
+
// Only resolving the instance can fail here; the pipeline hands every other error to the fallback
|
|
976
|
+
logError(error);
|
|
977
|
+
}
|
|
978
|
+
});
|
|
964
979
|
if (once) this.bot.once(event, listener);
|
|
965
980
|
else this.bot.on(event, listener);
|
|
966
981
|
}
|
|
@@ -1029,29 +1044,35 @@ class MeoCordApp {
|
|
|
1029
1044
|
*/ async runReadyHooks(client) {
|
|
1030
1045
|
const entries = [];
|
|
1031
1046
|
this.lifecycleEntries = entries;
|
|
1032
|
-
await startupRoots.runReadyHooks(this.container, this.lifecycleUnits, client, {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1047
|
+
await this.readyPass.run(()=>startupRoots.runReadyHooks(this.container, this.lifecycleUnits, client, {
|
|
1048
|
+
primary: client.shard ? client.shard.ids.includes(0) : true
|
|
1049
|
+
}, entries, {
|
|
1050
|
+
resolveFailed: (unit, error)=>this.logger.error(`Could not resolve ${unit.name} to run its lifecycle hooks:`, error),
|
|
1051
|
+
hookFailed: (unit, error)=>this.logger.error(`onReady failed in ${unit.name}:`, error),
|
|
1052
|
+
dependsOnFailed: (unit, failed)=>this.logger.warn(`Running onReady in ${unit.name} although it depends on ${failed.map(({ name })=>name).join(', ')}, which failed.`),
|
|
1053
|
+
slow: (unit)=>this.logger.warn(`onReady in ${unit.name} has run for over ${SLOW_READY_HOOK_MS} ms; the hooks after it are waiting.`)
|
|
1054
|
+
}, // Shutdown has begun: the client is going away, so no further hook starts
|
|
1055
|
+
{
|
|
1056
|
+
stopped: ()=>this.closing,
|
|
1057
|
+
slowAfterMs: SLOW_READY_HOOK_MS,
|
|
1058
|
+
settled: (unit)=>unit.cooldownStore && this.storeReady?.resolve(),
|
|
1059
|
+
runOnReady: this.readyPass.runOnReady
|
|
1060
|
+
}));
|
|
1045
1061
|
// Also when shutdown stopped the hooks before the store's, so no call waits on it
|
|
1046
1062
|
this.storeReady?.resolve();
|
|
1047
1063
|
}
|
|
1048
|
-
/** Runs the `onShutdown` hooks through the shutdown sequence, within the configured `shutdownTimeout`. */ async runShutdownHooks(
|
|
1049
|
-
|
|
1050
|
-
|
|
1064
|
+
/** Runs the `onShutdown` hooks through the shutdown sequence, within the configured `shutdownTimeout`. */ async runShutdownHooks() {
|
|
1065
|
+
// Read once the onReady in progress settled; a login that failed never ran onReady, so there is no onShutdown to undo
|
|
1066
|
+
await startupRoots.runShutdownSequence(this.container, ()=>this.lifecycleEntries ?? [], {
|
|
1067
|
+
// No new call starts once the listeners are gone; an error the client emits meanwhile still has its listeners
|
|
1051
1068
|
drainCalls: ()=>{
|
|
1052
|
-
this.bot
|
|
1053
|
-
|
|
1069
|
+
const emitter = this.bot;
|
|
1070
|
+
for (const event of emitter.eventNames())if (event !== 'error') emitter.removeAllListeners(event);
|
|
1071
|
+
return this.calls.settled();
|
|
1054
1072
|
},
|
|
1073
|
+
drainAlways: true,
|
|
1074
|
+
runningCalls: ()=>this.calls.running(),
|
|
1075
|
+
readyPass: this.readyPass,
|
|
1055
1076
|
timeoutMs: this.shutdownTimeout,
|
|
1056
1077
|
hookFailed: (name, error)=>this.logger.error(`onShutdown failed in ${name}:`, error),
|
|
1057
1078
|
warn: (message)=>this.logger.warn(message)
|
|
@@ -1070,8 +1091,28 @@ class MeoCordApp {
|
|
|
1070
1091
|
providers.releaseAmbientAppTheme(this.container);
|
|
1071
1092
|
}
|
|
1072
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Waits for the startup under way to stop making providers and services, at most `shutdownTimeout`; past it, warns
|
|
1096
|
+
* with the factory it still awaits, which then makes nothing more.
|
|
1097
|
+
*/ async waitForStartup(preparing) {
|
|
1098
|
+
let timer;
|
|
1099
|
+
const outlasted = new Promise((resolve)=>{
|
|
1100
|
+
timer = setTimeout(()=>resolve(true), this.shutdownTimeout);
|
|
1101
|
+
});
|
|
1102
|
+
const settled = preparing.then(()=>false, ()=>false);
|
|
1103
|
+
if (await Promise.race([
|
|
1104
|
+
settled,
|
|
1105
|
+
outlasted
|
|
1106
|
+
])) {
|
|
1107
|
+
const what = this.pendingProvider ? `the factory providing ${this.pendingProvider}` : 'its providers';
|
|
1108
|
+
this.logger.warn(`The startup still waits for ${what} after ${this.shutdownTimeout} ms; shutting down without it.`);
|
|
1109
|
+
}
|
|
1110
|
+
clearTimeout(timer);
|
|
1111
|
+
}
|
|
1073
1112
|
async shutDownClient() {
|
|
1074
1113
|
this.logger.log('Shutting down bot...');
|
|
1114
|
+
// So nothing is made after the bot has shut down
|
|
1115
|
+
if (this.preparing) await this.waitForStartup(this.preparing);
|
|
1075
1116
|
// Nothing came online, so there are no hooks to undo; the listeners go first, so none of them runs
|
|
1076
1117
|
if (this.abortLogin) {
|
|
1077
1118
|
this.logger.log('The bot was still logging in, so it stops without coming online');
|
|
@@ -1081,8 +1122,8 @@ class MeoCordApp {
|
|
|
1081
1122
|
return true;
|
|
1082
1123
|
}
|
|
1083
1124
|
if (this.activityInterval) clearInterval(this.activityInterval);
|
|
1084
|
-
//
|
|
1085
|
-
|
|
1125
|
+
// The calls drain on every shutdown, and the onReady in progress settles first
|
|
1126
|
+
await this.runShutdownHooks();
|
|
1086
1127
|
try {
|
|
1087
1128
|
this.bot.removeAllListeners();
|
|
1088
1129
|
await this.bot.destroy();
|
|
@@ -1093,7 +1134,7 @@ class MeoCordApp {
|
|
|
1093
1134
|
return false;
|
|
1094
1135
|
}
|
|
1095
1136
|
}
|
|
1096
|
-
constructor(controllerClasses, container, discordClient, discordToken, activities, lifecycleClasses = [], shutdownTimeout, startup, lifecycleUnits, messageOptions = {}, warnUnanswered = false){
|
|
1137
|
+
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){
|
|
1097
1138
|
this.controllerClasses = controllerClasses;
|
|
1098
1139
|
this.container = container;
|
|
1099
1140
|
this.discordClient = discordClient;
|
|
@@ -1102,6 +1143,7 @@ class MeoCordApp {
|
|
|
1102
1143
|
this.lifecycleClasses = lifecycleClasses;
|
|
1103
1144
|
this.startup = startup;
|
|
1104
1145
|
this.messageOptions = messageOptions;
|
|
1146
|
+
this.shard = shard;
|
|
1105
1147
|
this.logger = new controller_decorator.Logger(MeoCordApp.name);
|
|
1106
1148
|
this.fallback = startupRoots.createFallback(this.logger, ()=>this.messageOptions, {
|
|
1107
1149
|
cooldownNotice: (refusal)=>providers.claimCooldownNotice(this.container, refusal),
|
|
@@ -1114,7 +1156,8 @@ class MeoCordApp {
|
|
|
1114
1156
|
* Closes the app once, however many stops and signals ask: whether the client was destroyed cleanly. The app stays
|
|
1115
1157
|
* among the running ones until the close settles, so a signal or a manager's request meanwhile waits for it.
|
|
1116
1158
|
*/ this.close = ()=>this.stopped ?? (this.stopped = this.closeClient().finally(()=>runningApps.delete(this.close)));
|
|
1117
|
-
/** The calls under way,
|
|
1159
|
+
/** The calls under way, which shutdown waits for, less one that asked the app to stop. */ this.calls = new startupRoots.RunningCalls();
|
|
1160
|
+
/** The ready hooks' pass, whose `onReady` in progress shutdown waits for, less one that asked the app to stop. */ this.readyPass = new startupRoots.ReadyPass();
|
|
1118
1161
|
/** Whether the providers, services and event handlers are set up, which a retry after a failed login reuses. */ this.prepared = false;
|
|
1119
1162
|
/** Whether the bot logged in, after which `start()` has nothing to do. */ this.online = false;
|
|
1120
1163
|
/** Whether a login of this app failed, so a retry restores the client discord.js destroyed. */ this.loginFailed = false;
|
|
@@ -1204,66 +1247,6 @@ class MeoCordApp {
|
|
|
1204
1247
|
return Object.fromEntries(Object.entries(node_util.parseEnv(text)).filter((entry)=>entry[1] !== undefined));
|
|
1205
1248
|
}
|
|
1206
1249
|
|
|
1207
|
-
/**
|
|
1208
|
-
* File written beside a bundle that carries native addons, naming the platform it was built for.
|
|
1209
|
-
*/ const PLATFORM_MANIFEST = 'meocord.platform.json';
|
|
1210
|
-
/** The C library this process runs against, when the runtime can say. */ function detectLibc() {
|
|
1211
|
-
if (process.platform !== 'linux') return undefined;
|
|
1212
|
-
try {
|
|
1213
|
-
const report = process.report?.getReport?.();
|
|
1214
|
-
if (!report?.header) return undefined;
|
|
1215
|
-
return report.header.glibcVersionRuntime ? 'glibc' : 'musl';
|
|
1216
|
-
} catch {
|
|
1217
|
-
return undefined;
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
/** The platform this process is running on. */ function currentPlatform() {
|
|
1221
|
-
const libc = detectLibc();
|
|
1222
|
-
return {
|
|
1223
|
-
platform: process.platform,
|
|
1224
|
-
arch: process.arch,
|
|
1225
|
-
...libc && {
|
|
1226
|
-
libc
|
|
1227
|
-
}
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
/** `linux-x64 (glibc)`, `darwin-arm64`. */ function describePlatform({ platform, arch, libc }) {
|
|
1231
|
-
return `${platform}-${arch}${libc ? ` (${libc})` : ''}`;
|
|
1232
|
-
}
|
|
1233
|
-
/**
|
|
1234
|
-
* Whether native binaries built for one platform can load on another.
|
|
1235
|
-
*
|
|
1236
|
-
* The C library is compared only when both sides are known, so a runtime that cannot report it
|
|
1237
|
-
* is not refused on a guess.
|
|
1238
|
-
*/ function isSamePlatform(built, running) {
|
|
1239
|
-
if (built.platform !== running.platform || built.arch !== running.arch) return false;
|
|
1240
|
-
return !built.libc || !running.libc || built.libc === running.libc;
|
|
1241
|
-
}
|
|
1242
|
-
/**
|
|
1243
|
-
* Stops a bundle from starting on a platform its native addons were not built for, with one clear
|
|
1244
|
-
* message instead of a linker error on the first command that loads an addon.
|
|
1245
|
-
* @param distDir - Directory holding the manifest. Defaults to the built bundle's directory, which a process manager's
|
|
1246
|
-
* wrapper in `argv[1]` would hide, and else to the entry script's.
|
|
1247
|
-
*/ function assertBuiltForThisPlatform(distDir = defaultDistDir()) {
|
|
1248
|
-
if (!distDir) return;
|
|
1249
|
-
const manifest = path__default$1.default.join(distDir, PLATFORM_MANIFEST);
|
|
1250
|
-
if (!fs.existsSync(manifest)) return;
|
|
1251
|
-
let built;
|
|
1252
|
-
try {
|
|
1253
|
-
built = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
1254
|
-
} catch {
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
const running = currentPlatform();
|
|
1258
|
-
if (isSamePlatform(built, running)) return;
|
|
1259
|
-
throw controller_decorator.refuse(new Error(`${path__default$1.default.basename(distDir)}: this build carries native addons compiled for ${describePlatform(built)}, but is ` + `running on ${describePlatform(running)}. Compiled binaries only load on the platform they were built for. ` + 'Build on the same platform you deploy to -- for a container, run `meocord build` inside the image.'));
|
|
1260
|
-
}
|
|
1261
|
-
/** The built bundle's directory, or the entry script's outside a built bot. */ function defaultDistDir() {
|
|
1262
|
-
const entry = controller_decorator.isBuiltApplication() ? controller_decorator.bundleEntry() : undefined;
|
|
1263
|
-
if (entry) return path__default$1.default.dirname(entry);
|
|
1264
|
-
return process.argv[1] ? path__default$1.default.dirname(process.argv[1]) : undefined;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
1250
|
/** How long the manager waits for a shard to become ready before spawning the next. */ const SHARD_READY_TIMEOUT_MS = 30000;
|
|
1268
1251
|
/** The pause between spawning two shards, as Discord's identify rate limit asks. */ const SHARD_SPAWN_DELAY_MS = 5500;
|
|
1269
1252
|
/** The first respawn delay, doubled for each exit in a row. */ const RESPAWN_BASE_MS = 1000;
|
|
@@ -1300,7 +1283,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1300
1283
|
process.on('SIGINT', ()=>void this.stopAndExit());
|
|
1301
1284
|
process.on('SIGTERM', ()=>void this.stopAndExit());
|
|
1302
1285
|
// How `meocord start --dev` stops the bot to restart it; no stop request of the user's, whose first Ctrl+C joins it
|
|
1303
|
-
onDevRunnerStop(()=>void this.exitAfterShutdown());
|
|
1286
|
+
onDevRunnerStop(()=>void this.exitAfterShutdown(), false);
|
|
1304
1287
|
let total;
|
|
1305
1288
|
try {
|
|
1306
1289
|
const shards = config.sharding?.shards ?? 'auto';
|
|
@@ -1317,6 +1300,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1317
1300
|
const manager = (this.options.createManager ?? ((path, opts)=>new discord_js.ShardingManager(path, opts)))(file, {
|
|
1318
1301
|
token,
|
|
1319
1302
|
totalShards: total,
|
|
1303
|
+
// discord.js refuses a call to every shard unless shardList counts the shards; createShard leaves it 'auto'
|
|
1304
|
+
shardList: [
|
|
1305
|
+
...Array(total).keys()
|
|
1306
|
+
],
|
|
1320
1307
|
mode: 'process',
|
|
1321
1308
|
execArgv: process.execArgv,
|
|
1322
1309
|
respawn: false
|
|
@@ -1413,12 +1400,12 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1413
1400
|
void this.exitForLogin();
|
|
1414
1401
|
}
|
|
1415
1402
|
/** Exits 1 for a bot that could not log in, telling `meocord start --dev` first, which then waits for a change. */ exitForLogin() {
|
|
1416
|
-
if (!
|
|
1403
|
+
if (!this.devRunner) return this.exit(1);
|
|
1417
1404
|
// Set first, so the process exits 1 even if it ends while the message is still on its way
|
|
1418
1405
|
process.exitCode = 1;
|
|
1419
1406
|
return tellDevRunner({
|
|
1420
1407
|
meocord: 'login-failed'
|
|
1421
|
-
}).then(()=>this.exit(1));
|
|
1408
|
+
}, false).then(()=>this.exit(1));
|
|
1422
1409
|
}
|
|
1423
1410
|
/**
|
|
1424
1411
|
* Asks every shard to shut down through its own hooks, waits for them up to the shutdown timeout plus a margin, and
|
|
@@ -1495,6 +1482,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1495
1482
|
/** Every shard's cooldown calls, for an app whose store is ShardedCooldownStore. */ this.cooldowns = new controller_decorator.MemoryCooldownStore();
|
|
1496
1483
|
this.stopping = false;
|
|
1497
1484
|
this.fatal = false;
|
|
1485
|
+
/**
|
|
1486
|
+
* Whether `meocord start --dev` runs the manager, decided before discord.js's ShardingManager, which writes the
|
|
1487
|
+
* variable that marks a shard into this process too.
|
|
1488
|
+
*/ this.devRunner = underDevRunner(false);
|
|
1498
1489
|
// The manager holds the credential for its shards and logs their failures, without an app of its own to register it
|
|
1499
1490
|
controller_decorator.hideInLogs(options.token);
|
|
1500
1491
|
this.exit = options.exit ?? ((code)=>process.exit(code));
|
|
@@ -1560,8 +1551,8 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1560
1551
|
* The config a single process runs with. Under `meocord start --dev` without `sharding.development`,
|
|
1561
1552
|
* process sharding falls back to running every shard in this process, so the watcher restarts one
|
|
1562
1553
|
* process and leaves no shards behind.
|
|
1563
|
-
*/ static effectiveConfig(config) {
|
|
1564
|
-
if (config.sharding?.mode !== 'process' ||
|
|
1554
|
+
*/ static effectiveConfig(config, shard) {
|
|
1555
|
+
if (config.sharding?.mode !== 'process' || shard || shardedCooldownStore.processShardingEnabled(config)) return config;
|
|
1565
1556
|
this.logger.info("sharding.mode 'process' is off in development, so every shard runs in this process; set " + 'sharding.development: true to run them in separate processes.');
|
|
1566
1557
|
return {
|
|
1567
1558
|
...config,
|
|
@@ -1581,7 +1572,8 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1581
1572
|
* @returns The application, which `start()` logs in.
|
|
1582
1573
|
* @throws Error when the class has no `@MeoCord`, when the compiled config is missing beside the bundle or fails to
|
|
1583
1574
|
* load, naming the file and the reason, with what to do about it, when a provider cannot
|
|
1584
|
-
* be bound, such as one for a token MeoCord binds itself
|
|
1575
|
+
* be bound, such as one for a token MeoCord binds itself (a provider for `ThemeCache` or `ExecutionContext` is warned
|
|
1576
|
+
* about instead, and refused from the next major version, 5.0), when two handlers take one command, or two builder
|
|
1585
1577
|
* classes build one, naming both, and for any other mistake it refuses as the app loads, such as two component
|
|
1586
1578
|
* patterns that match the same customIds. Each check reports every error it finds, and the first is thrown,
|
|
1587
1579
|
* unchanged. When there are several, each is logged first, with where in the source it comes from; a lone one is
|
|
@@ -1589,11 +1581,14 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1589
1581
|
* shard of process sharding, the manager logs it. With `startupErrors: 'all'` in `meocord.config.ts`, the errors
|
|
1590
1582
|
* decorators kept are among them.
|
|
1591
1583
|
*/ static create(target) {
|
|
1584
|
+
// Decided first, once, and handed to all that asks: a ShardingManager this app builds writes the variable into its
|
|
1585
|
+
// own process too, which would make the manager read as a shard
|
|
1586
|
+
const shard = shardedCooldownStore.isShardProcess(process.env);
|
|
1592
1587
|
try {
|
|
1593
|
-
return this.createApplication(target);
|
|
1588
|
+
return this.createApplication(target, shard);
|
|
1594
1589
|
} catch (error) {
|
|
1595
1590
|
// A shard ends, so its manager restarts it, or stops every shard for a refusal, which it logs
|
|
1596
|
-
if (
|
|
1591
|
+
if (shard) endFailedShard(error);
|
|
1597
1592
|
else if (controller_decorator.isRefusal(error) && !controller_decorator.isExplainedError(error)) {
|
|
1598
1593
|
// Every startup error found together is logged, in a test too; a lone one is logged in a built application
|
|
1599
1594
|
if (controller_decorator.logStartupErrors(error, (text)=>this.logger.error(text), 'the bot did not start')) controller_decorator.markExplained(error);
|
|
@@ -1605,18 +1600,25 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1605
1600
|
throw error;
|
|
1606
1601
|
}
|
|
1607
1602
|
}
|
|
1608
|
-
static createApplication(target) {
|
|
1609
|
-
const
|
|
1603
|
+
static createApplication(target, shard) {
|
|
1604
|
+
const appOptions = Reflect.getMetadata(controller_decorator.META.appOptions, target);
|
|
1610
1605
|
// With startupErrors: 'all', what the decorators kept: a built bot's every one, else the app's and its controllers'
|
|
1611
1606
|
const declared = controller_decorator.isBuiltApplication() ? controller_decorator.allDeclaredErrors() : controller_decorator.declaredErrorsOf([
|
|
1612
1607
|
target,
|
|
1613
|
-
...
|
|
1608
|
+
...appOptions?.controllers ?? []
|
|
1614
1609
|
]);
|
|
1615
|
-
if (!
|
|
1610
|
+
if (!appOptions) {
|
|
1616
1611
|
// A refused @MeoCord stores no options, so its own errors say why
|
|
1617
1612
|
controller_decorator.collectStartupErrors(()=>declared.forEach(controller_decorator.startupError));
|
|
1618
1613
|
throw controller_decorator.refuse(new Error(`${typeof target === 'function' ? target.name : String(target)}: not decorated with @MeoCord(), so there is no app to create.`));
|
|
1619
1614
|
}
|
|
1615
|
+
// A controller listed twice is one controller, as the command checks and fromApp already take it
|
|
1616
|
+
const options = {
|
|
1617
|
+
...appOptions,
|
|
1618
|
+
controllers: [
|
|
1619
|
+
...new Set(appOptions.controllers)
|
|
1620
|
+
]
|
|
1621
|
+
};
|
|
1620
1622
|
const meocordConfig = controller_decorator.loadMeoCordConfig();
|
|
1621
1623
|
if (!meocordConfig) {
|
|
1622
1624
|
throw controller_decorator.refuse(new Error(controller_decorator.compiledConfigMessage()));
|
|
@@ -1626,7 +1628,9 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1626
1628
|
const providers$1 = controller_decorator.collectStartupErrors(()=>{
|
|
1627
1629
|
declared.forEach(controller_decorator.startupError);
|
|
1628
1630
|
startupRoots.assertDistinctCommands(options.controllers);
|
|
1629
|
-
|
|
1631
|
+
const providers$1 = providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
|
|
1632
|
+
providers.assertProvidedServices(typeof target === 'function' ? target.name : String(target), options.services ?? [], providers$1, '@MeoCord({ providers })');
|
|
1633
|
+
return providers$1;
|
|
1630
1634
|
});
|
|
1631
1635
|
// A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
|
|
1632
1636
|
const themeResolver = controller_decorator.themeResolverClass(options.themeFor);
|
|
@@ -1643,7 +1647,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1643
1647
|
...providers.appObservers(target)
|
|
1644
1648
|
];
|
|
1645
1649
|
// A shard's manager runs the same checks, so a sharded bot warns once
|
|
1646
|
-
if (!
|
|
1650
|
+
if (!shard) {
|
|
1647
1651
|
startupRoots.warnUnregisteredCommands(options.controllers);
|
|
1648
1652
|
startupRoots.warnInheritedRoutes(options.controllers);
|
|
1649
1653
|
startupRoots.warnOverlappingPatterns(options.controllers);
|
|
@@ -1658,11 +1662,8 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1658
1662
|
if (isRegisterOnly()) {
|
|
1659
1663
|
return new MeoCordApp(options.controllers, new inversify.Container(), new discord_js.Client(options.clientOptions), meocordConfig.discordToken);
|
|
1660
1664
|
}
|
|
1661
|
-
// Before anything is resolved: a controller or service is what first loads a native addon, and one built for another
|
|
1662
|
-
// platform would otherwise fail there with a linker error. A manager checks too, before it registers or spawns.
|
|
1663
|
-
assertBuiltForThisPlatform();
|
|
1664
1665
|
// A process-sharding manager only spawns shards, so it binds, constructs and connects nothing itself.
|
|
1665
|
-
if (shardedCooldownStore.shardingRole(meocordConfig) === 'manager') {
|
|
1666
|
+
if (shardedCooldownStore.shardingRole(meocordConfig, shard) === 'manager') {
|
|
1666
1667
|
return new ShardManager({
|
|
1667
1668
|
controllerClasses: options.controllers,
|
|
1668
1669
|
token: meocordConfig.discordToken,
|
|
@@ -1700,7 +1701,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1700
1701
|
], startupRoots.lateGuardCheck(providers$1, translator));
|
|
1701
1702
|
providers.bindGlobalStages(container, stages);
|
|
1702
1703
|
// Bound now, so no provider stands in for it; made once the checks pass, the one Client the app logs in with
|
|
1703
|
-
const clientOptions = shardedCooldownStore.clientOptionsWithSharding(this.effectiveConfig(meocordConfig), options.clientOptions);
|
|
1704
|
+
const clientOptions = shardedCooldownStore.clientOptionsWithSharding(this.effectiveConfig(meocordConfig, shard), options.clientOptions);
|
|
1704
1705
|
container.bind(discord_js.Client).toDynamicValue(()=>new discord_js.Client(clientOptions)).inSingletonScope();
|
|
1705
1706
|
if (options.i18n) {
|
|
1706
1707
|
container.bind(controller_decorator.Translator).toConstantValue(options.i18n);
|
|
@@ -1728,6 +1729,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1728
1729
|
// After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
|
|
1729
1730
|
// a class token that is provided is not also bound as itself
|
|
1730
1731
|
for (const [token, provider] of providers$1){
|
|
1732
|
+
// Bound later, or for each call, so not caught below; supported until 5.0 refuses it
|
|
1733
|
+
if (token === controller_decorator.ThemeCache || token === controller_decorator.ExecutionContext) {
|
|
1734
|
+
controller_decorator.warnOnce(this.logger, `${target.name}: @MeoCord({ providers }) provides ${providers.tokenName(token)}, which MeoCord binds itself; ` + 'the next major version (5.0) refuses it. Remove the provider.');
|
|
1735
|
+
}
|
|
1731
1736
|
if (container.isBound(token)) {
|
|
1732
1737
|
controller_decorator.startupError(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${providers.tokenName(token)}, which MeoCord binds itself.`));
|
|
1733
1738
|
continue;
|
|
@@ -1745,6 +1750,9 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1745
1750
|
// Observers are services too: bound here so their lifecycle hooks run in dependency order
|
|
1746
1751
|
const observers = providers.appObservers(target);
|
|
1747
1752
|
for (const observer of observers)bindDependencies(container, observer, providers$1);
|
|
1753
|
+
// What the stages and presenter inject, bound now so a service among them is made as the bot comes online
|
|
1754
|
+
const stageServices = startupRoots.stageDependencies(runs, providers$1, options.presenter);
|
|
1755
|
+
for (const service of stageServices)bindDependencies(container, service, providers$1);
|
|
1748
1756
|
// What follows walks what these bound, so a class refused here is not reported again as missing
|
|
1749
1757
|
controller_decorator.stopOnStartupErrors();
|
|
1750
1758
|
// Providers first, then the services, the controllers and the observers, each after what it depends on. The app's
|
|
@@ -1765,12 +1773,19 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1765
1773
|
return providers.isAppClassToken(token) && (!provider || providers.isClassProvider(provider) && provider.useClass === token);
|
|
1766
1774
|
}));
|
|
1767
1775
|
providers.assertProvided(container, providers$1, runs.classes, '@MeoCord({ providers })');
|
|
1776
|
+
// With what the stages inject among them, each after what it depends on, so their hooks run as a listed service's do
|
|
1777
|
+
const roots = stageServices.length > 0 ? providers.resolutionOrder(container, providers$1, [
|
|
1778
|
+
...order,
|
|
1779
|
+
...stageServices
|
|
1780
|
+
], {
|
|
1781
|
+
followOwnTokens: false
|
|
1782
|
+
}) : order;
|
|
1768
1783
|
// The store first, after only what it injects: it is ready before anything a call reaches, and shuts down last
|
|
1769
1784
|
const store = options.cooldownStore;
|
|
1770
1785
|
const lifecycle = (store ? providers.resolutionOrder(container, providers$1, [
|
|
1771
1786
|
store,
|
|
1772
|
-
...
|
|
1773
|
-
]) :
|
|
1787
|
+
...roots
|
|
1788
|
+
]) : roots).map((token)=>({
|
|
1774
1789
|
token,
|
|
1775
1790
|
name: providers.tokenName(token),
|
|
1776
1791
|
dependencies: providers.tokenDependencies(container, providers$1, token),
|
|
@@ -1810,24 +1825,28 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1810
1825
|
for (const cls of runs.classes){
|
|
1811
1826
|
Reflect.defineMetadata(controller_decorator.META.container, container, cls);
|
|
1812
1827
|
}
|
|
1828
|
+
providers.recordInstanceContainers(container, runs.classes);
|
|
1813
1829
|
providers.prepareThemes(container, appClasses);
|
|
1814
1830
|
// Run by start() before it logs in: what may inject a provided value is resolved once every factory
|
|
1815
1831
|
// has made its value, including those that return a promise
|
|
1816
1832
|
const logger = this.logger;
|
|
1817
|
-
const startup = async ()=>{
|
|
1833
|
+
const startup = async (control)=>{
|
|
1818
1834
|
try {
|
|
1819
|
-
await providers.resolveProviders(container, providers$1, order);
|
|
1835
|
+
await providers.resolveProviders(container, providers$1, order, control);
|
|
1820
1836
|
} catch (error) {
|
|
1821
1837
|
logger.error(`${error.message}. The bot cannot start without it.`);
|
|
1822
1838
|
logger.debug('Provider failure:', error.cause);
|
|
1823
1839
|
controller_decorator.markExplained(error);
|
|
1824
1840
|
throw error;
|
|
1825
1841
|
}
|
|
1826
|
-
// The listed services are made now, so constructors that attach listeners or connect run before login
|
|
1842
|
+
// The listed services are made now, so constructors that attach listeners or connect run before login; a stop
|
|
1843
|
+
// meanwhile makes no more of them
|
|
1827
1844
|
for (const svc of options.services ?? []){
|
|
1845
|
+
if (control.stopped()) return;
|
|
1828
1846
|
container.get(svc);
|
|
1829
1847
|
}
|
|
1830
|
-
if (
|
|
1848
|
+
if (control.stopped()) return;
|
|
1849
|
+
if (shard && container.get(controller_decorator.CooldownStore) instanceof controller_decorator.MemoryCooldownStore) {
|
|
1831
1850
|
warnPerShardCooldowns(options.controllers, logger);
|
|
1832
1851
|
}
|
|
1833
1852
|
providers.bindAppPresenter(container, target, discordClient);
|
|
@@ -1835,7 +1854,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1835
1854
|
controller_decorator.registerClientTranslator(discordClient, options.i18n);
|
|
1836
1855
|
};
|
|
1837
1856
|
return new MeoCordApp(options.controllers, container, discordClient, meocordConfig.discordToken, options.activities, appClasses, meocordConfig.shutdownTimeout, startup, lifecycle, options.messages, // In development only, unless the app says otherwise
|
|
1838
|
-
options.warnUnanswered ?? process.env.NODE_ENV === 'development');
|
|
1857
|
+
options.warnUnanswered ?? process.env.NODE_ENV === 'development', shard);
|
|
1839
1858
|
});
|
|
1840
1859
|
}
|
|
1841
1860
|
}
|