meocord 4.1.2 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +362 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
- package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
- package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
- package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
- package/dist/cjs/common/index.cjs +4 -4
- package/dist/cjs/core/index.cjs +365 -310
- package/dist/cjs/decorator/index.cjs +85 -61
- package/dist/cjs/testing/index.cjs +2236 -496
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/README.md.template +3 -1
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
- package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
- package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
- package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +17 -4
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/decorator.js +6 -6
- package/dist/esm/common/deprecation.js +2 -2
- package/dist/esm/common/errors.js +27 -2
- package/dist/esm/common/execution-context.js +1 -1
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/metadata.js +1 -1
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +84 -10
- package/dist/esm/common/route.js +21 -5
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translate-error.js +1 -1
- package/dist/esm/common/translator.js +37 -6
- package/dist/esm/core/command-conflicts.js +34 -34
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/component-routes.js +74 -31
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +51 -7
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +27 -14
- package/dist/esm/core/handler-registry.js +1 -1
- package/dist/esm/core/interceptor-runner.js +2 -2
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +241 -179
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-routes.js +4 -3
- package/dist/esm/core/observer-runner.js +4 -3
- package/dist/esm/core/providers.js +45 -23
- package/dist/esm/core/route-specificity.js +4 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/scalar-types.js +15 -3
- package/dist/esm/core/shard-context.js +6 -7
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/startup-roots.js +23 -4
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-runtime.js +2 -2
- package/dist/esm/core/theme-scope.js +3 -1
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +12 -7
- package/dist/esm/decorator/command-builder.decorator.js +4 -4
- package/dist/esm/decorator/controller-class.decorator.js +9 -4
- package/dist/esm/decorator/controller.decorator.js +162 -83
- package/dist/esm/decorator/cooldown.decorator.js +20 -3
- package/dist/esm/decorator/defer.decorator.js +4 -4
- package/dist/esm/decorator/event.decorator.js +5 -5
- package/dist/esm/decorator/filter.decorator.js +7 -7
- package/dist/esm/decorator/guard.decorator.js +11 -10
- package/dist/esm/decorator/inject.decorator.js +4 -4
- package/dist/esm/decorator/interceptor.decorator.js +7 -7
- package/dist/esm/decorator/observer.decorator.js +4 -4
- package/dist/esm/decorator/service.decorator.js +4 -4
- package/dist/esm/decorator/theme.decorator.js +3 -3
- package/dist/esm/decorator/validation.decorator.js +10 -10
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +27 -11
- package/dist/esm/testing/cooldown-store-suite.js +85 -15
- package/dist/esm/testing/discord-defaults.js +249 -21
- package/dist/esm/testing/index.js +4 -2
- package/dist/esm/testing/inspect-handler.js +20 -3
- package/dist/esm/testing/meocord-testing-module.js +352 -205
- package/dist/esm/testing/mock-fn.js +173 -22
- package/dist/esm/testing/mock-interaction.js +1214 -209
- package/dist/esm/testing/response.js +1 -1
- package/dist/esm/testing/routing.js +39 -14
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/startup-errors.js +26 -0
- package/dist/esm/testing/strict-mocks.js +65 -0
- package/dist/esm/testing/theme.js +1 -1
- package/dist/esm/util/bundle-entry.util.js +2 -1
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +3 -3
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +4 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/metadata-keys.js +1 -0
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/refusal.util.js +148 -8
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +12 -1
- package/dist/types/common/index.d.cts +8 -8
- package/dist/types/common/index.d.ts +8 -8
- package/dist/types/core/index.d.cts +10 -7
- package/dist/types/core/index.d.ts +10 -7
- package/dist/types/decorator/index.d.cts +31 -29
- package/dist/types/decorator/index.d.ts +31 -29
- package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
- package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
- package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
- package/dist/types/testing/index.d.cts +406 -70
- package/dist/types/testing/index.d.ts +406 -70
- package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +4 -3
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-BZgJyfZS.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-XKMlcUZz.cjs');
|
|
8
8
|
require('node:crypto');
|
|
9
|
-
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-
|
|
10
|
-
var
|
|
11
|
-
var startupRoots = require('../_shared/startup-roots-lFc69AxC.cjs');
|
|
9
|
+
var shardedCooldownStore = require('../_shared/sharded-cooldown-store-CsJJyCZd.cjs');
|
|
10
|
+
var startupRoots = require('../_shared/startup-roots-CgfPQrNl.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-BUAR1WYG.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));
|
|
@@ -1510,14 +1501,14 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1510
1501
|
*/ function bindDependencies(container, cls, providers$1) {
|
|
1511
1502
|
// A provided class is bound by its own provider, wherever in the list that provider comes
|
|
1512
1503
|
if (container.isBound(cls) || providers$1.has(cls)) return;
|
|
1513
|
-
if (providers.injectedTokens(cls).includes(controller_decorator.ExecutionContext))
|
|
1504
|
+
if (providers.injectedTokens(cls).includes(controller_decorator.ExecutionContext)) return controller_decorator.startupError(providers.singletonContextError(cls));
|
|
1514
1505
|
providers.assertStartupChecked(container, cls);
|
|
1515
1506
|
providers.makeInjectable(cls);
|
|
1516
1507
|
container.bind(cls).toSelf().inSingletonScope();
|
|
1517
1508
|
// By constructor type or @inject token; an interface-typed parameter records Object, which is skipped
|
|
1518
1509
|
for (const dep of providers.injectedTokens(cls)){
|
|
1519
|
-
if (dep === controller_decorator.Translator && !container.isBound(controller_decorator.Translator))
|
|
1520
|
-
if (providers.isAppClassToken(dep)) bindDependencies(container, dep, providers$1);
|
|
1510
|
+
if (dep === controller_decorator.Translator && !container.isBound(controller_decorator.Translator)) controller_decorator.startupError(controller_decorator.missingTranslatorError(cls));
|
|
1511
|
+
else if (providers.isAppClassToken(dep)) bindDependencies(container, dep, providers$1);
|
|
1521
1512
|
}
|
|
1522
1513
|
}
|
|
1523
1514
|
/**
|
|
@@ -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,36 +1572,64 @@ 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
|
-
* patterns that match the same customIds.
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1578
|
+
* patterns that match the same customIds. Each check reports every error it finds, and the first is thrown,
|
|
1579
|
+
* unchanged. When there are several, each is logged first, with where in the source it comes from; a lone one is
|
|
1580
|
+
* logged in a built application. Either way `isExplainedError()` then tells a caller not to log it again; in a
|
|
1581
|
+
* shard of process sharding, the manager logs it. With `startupErrors: 'all'` in `meocord.config.ts`, the errors
|
|
1582
|
+
* decorators kept are among them.
|
|
1589
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);
|
|
1590
1587
|
try {
|
|
1591
|
-
return this.createApplication(target);
|
|
1588
|
+
return this.createApplication(target, shard);
|
|
1592
1589
|
} catch (error) {
|
|
1593
1590
|
// A shard ends, so its manager restarts it, or stops every shard for a refusal, which it logs
|
|
1594
|
-
if (
|
|
1595
|
-
else if (controller_decorator.
|
|
1596
|
-
|
|
1597
|
-
controller_decorator.markExplained(error);
|
|
1591
|
+
if (shard) endFailedShard(error);
|
|
1592
|
+
else if (controller_decorator.isRefusal(error) && !controller_decorator.isExplainedError(error)) {
|
|
1593
|
+
// Every startup error found together is logged, in a test too; a lone one is logged in a built application
|
|
1594
|
+
if (controller_decorator.logStartupErrors(error, (text)=>this.logger.error(text), 'the bot did not start')) controller_decorator.markExplained(error);
|
|
1595
|
+
else if (controller_decorator.isBuiltApplication()) {
|
|
1596
|
+
this.logger.error(controller_decorator.describeRefusal(error, process.cwd()));
|
|
1597
|
+
controller_decorator.markExplained(error);
|
|
1598
|
+
}
|
|
1598
1599
|
}
|
|
1599
1600
|
throw error;
|
|
1600
1601
|
}
|
|
1601
1602
|
}
|
|
1602
|
-
static createApplication(target) {
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1603
|
+
static createApplication(target, shard) {
|
|
1604
|
+
const appOptions = Reflect.getMetadata(controller_decorator.META.appOptions, target);
|
|
1605
|
+
// With startupErrors: 'all', what the decorators kept: a built bot's every one, else the app's and its controllers'
|
|
1606
|
+
const declared = controller_decorator.isBuiltApplication() ? controller_decorator.allDeclaredErrors() : controller_decorator.declaredErrorsOf([
|
|
1607
|
+
target,
|
|
1608
|
+
...appOptions?.controllers ?? []
|
|
1609
|
+
]);
|
|
1610
|
+
if (!appOptions) {
|
|
1611
|
+
// A refused @MeoCord stores no options, so its own errors say why
|
|
1612
|
+
controller_decorator.collectStartupErrors(()=>declared.forEach(controller_decorator.startupError));
|
|
1605
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.`));
|
|
1606
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
|
+
};
|
|
1607
1622
|
const meocordConfig = controller_decorator.loadMeoCordConfig();
|
|
1608
1623
|
if (!meocordConfig) {
|
|
1609
1624
|
throw controller_decorator.refuse(new Error(controller_decorator.compiledConfigMessage()));
|
|
1610
1625
|
}
|
|
1611
|
-
// Before any of the three ways a bot runs, so none registers or dispatches a command only one handler could take
|
|
1612
|
-
|
|
1613
|
-
const providers$1 =
|
|
1626
|
+
// Before any of the three ways a bot runs, so none registers or dispatches a command only one handler could take; every
|
|
1627
|
+
// error each check finds is reported, the first thrown
|
|
1628
|
+
const providers$1 = controller_decorator.collectStartupErrors(()=>{
|
|
1629
|
+
declared.forEach(controller_decorator.startupError);
|
|
1630
|
+
startupRoots.assertDistinctCommands(options.controllers);
|
|
1631
|
+
return providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
|
|
1632
|
+
});
|
|
1614
1633
|
// A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
|
|
1615
1634
|
const themeResolver = controller_decorator.themeResolverClass(options.themeFor);
|
|
1616
1635
|
// The app's classes and every class they inject, which the container binds
|
|
@@ -1626,7 +1645,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1626
1645
|
...providers.appObservers(target)
|
|
1627
1646
|
];
|
|
1628
1647
|
// A shard's manager runs the same checks, so a sharded bot warns once
|
|
1629
|
-
if (!
|
|
1648
|
+
if (!shard) {
|
|
1630
1649
|
startupRoots.warnUnregisteredCommands(options.controllers);
|
|
1631
1650
|
startupRoots.warnInheritedRoutes(options.controllers);
|
|
1632
1651
|
startupRoots.warnOverlappingPatterns(options.controllers);
|
|
@@ -1641,164 +1660,200 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
1641
1660
|
if (isRegisterOnly()) {
|
|
1642
1661
|
return new MeoCordApp(options.controllers, new inversify.Container(), new discord_js.Client(options.clientOptions), meocordConfig.discordToken);
|
|
1643
1662
|
}
|
|
1644
|
-
// Before anything is resolved: a controller or service is what first loads a native addon, and one built for another
|
|
1645
|
-
// platform would otherwise fail there with a linker error. A manager checks too, before it registers or spawns.
|
|
1646
|
-
assertBuiltForThisPlatform();
|
|
1647
1663
|
// A process-sharding manager only spawns shards, so it binds, constructs and connects nothing itself.
|
|
1648
|
-
if (shardedCooldownStore.shardingRole(meocordConfig) === 'manager') {
|
|
1664
|
+
if (shardedCooldownStore.shardingRole(meocordConfig, shard) === 'manager') {
|
|
1649
1665
|
return new ShardManager({
|
|
1650
1666
|
controllerClasses: options.controllers,
|
|
1651
1667
|
token: meocordConfig.discordToken,
|
|
1652
1668
|
config: meocordConfig
|
|
1653
1669
|
});
|
|
1654
1670
|
}
|
|
1655
|
-
//
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
container.bind(providers.COOLDOWN_POLICY).toConstantValue(providers.cooldownPolicyFrom(options));
|
|
1692
|
-
// A store of the app's own is resolved like a service, so it can inject its client
|
|
1693
|
-
if (options.cooldownStore) {
|
|
1694
|
-
bindDependencies(container, options.cooldownStore, providers$1);
|
|
1695
|
-
container.bind(controller_decorator.CooldownStore).toService(options.cooldownStore);
|
|
1696
|
-
} else {
|
|
1697
|
-
container.bind(controller_decorator.CooldownStore).toConstantValue(new controller_decorator.MemoryCooldownStore());
|
|
1698
|
-
}
|
|
1699
|
-
// A class that injects the token depends on the app's store, the one unit whose hooks run
|
|
1700
|
-
providers.bindsOwnToken(container, controller_decorator.CooldownStore, options.cooldownStore);
|
|
1701
|
-
// After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
|
|
1702
|
-
// a class token that is provided is not also bound as itself
|
|
1703
|
-
for (const [token, provider] of providers$1){
|
|
1704
|
-
if (container.isBound(token)) {
|
|
1705
|
-
throw controller_decorator.refuse(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${providers.tokenName(token)}, which MeoCord binds itself.`));
|
|
1671
|
+
// The checks of what the app binds and runs report every error each finds, and the first is thrown
|
|
1672
|
+
return controller_decorator.collectStartupErrors(()=>{
|
|
1673
|
+
// Every class the app runs, its stages and presenter included, checked before binding, where inversify would
|
|
1674
|
+
// otherwise fail first with an error about compiler options, or a guard only at its first call
|
|
1675
|
+
const stages = providers.appStages(target);
|
|
1676
|
+
const runs = startupRoots.startupClasses({
|
|
1677
|
+
controllers: options.controllers,
|
|
1678
|
+
services: options.services ?? [],
|
|
1679
|
+
providers: providers$1,
|
|
1680
|
+
stages,
|
|
1681
|
+
presenter: options.presenter,
|
|
1682
|
+
cooldownStore: options.cooldownStore,
|
|
1683
|
+
themeFor: options.themeFor,
|
|
1684
|
+
observers: providers.appObservers(target)
|
|
1685
|
+
});
|
|
1686
|
+
startupRoots.assertStartupClasses(runs, {
|
|
1687
|
+
translator: options.i18n !== undefined || providers$1.has(controller_decorator.Translator)
|
|
1688
|
+
});
|
|
1689
|
+
// Outside a built application, the kept errors of the stages and other classes the app's classes reach
|
|
1690
|
+
if (!controller_decorator.isBuiltApplication()) controller_decorator.declaredErrorsOf(runs.classes).forEach(controller_decorator.startupError);
|
|
1691
|
+
// The checks of the wiring below run on classes that passed these, so a mistake in one is not reported again there
|
|
1692
|
+
controller_decorator.stopOnStartupErrors();
|
|
1693
|
+
// The checks bind into this container and change nothing else: what they bind is made once all have passed
|
|
1694
|
+
const container = new inversify.Container();
|
|
1695
|
+
const translator = options.i18n !== undefined || providers$1.has(controller_decorator.Translator);
|
|
1696
|
+
providers.markStartupChecked(container, [
|
|
1697
|
+
...runs.classes,
|
|
1698
|
+
...providers.meocordClasses()
|
|
1699
|
+
], startupRoots.lateGuardCheck(providers$1, translator));
|
|
1700
|
+
providers.bindGlobalStages(container, stages);
|
|
1701
|
+
// Bound now, so no provider stands in for it; made once the checks pass, the one Client the app logs in with
|
|
1702
|
+
const clientOptions = shardedCooldownStore.clientOptionsWithSharding(this.effectiveConfig(meocordConfig, shard), options.clientOptions);
|
|
1703
|
+
container.bind(discord_js.Client).toDynamicValue(()=>new discord_js.Client(clientOptions)).inSingletonScope();
|
|
1704
|
+
if (options.i18n) {
|
|
1705
|
+
container.bind(controller_decorator.Translator).toConstantValue(options.i18n);
|
|
1706
|
+
providers.bindsOwnToken(container, controller_decorator.Translator);
|
|
1706
1707
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
});
|
|
1733
|
-
appClasses.push(...order.filter((token)=>{
|
|
1734
|
-
const provider = providers$1.get(token);
|
|
1735
|
-
return providers.isAppClassToken(token) && (!provider || providers.isClassProvider(provider) && provider.useClass === token);
|
|
1736
|
-
}));
|
|
1737
|
-
providers.assertProvided(container, providers$1, runs.classes, '@MeoCord({ providers })');
|
|
1738
|
-
// The store first, after only what it injects: it is ready before anything a call reaches, and shuts down last
|
|
1739
|
-
const store = options.cooldownStore;
|
|
1740
|
-
const lifecycle = (store ? providers.resolutionOrder(container, providers$1, [
|
|
1741
|
-
store,
|
|
1742
|
-
...order
|
|
1743
|
-
]) : order).map((token)=>({
|
|
1744
|
-
token,
|
|
1745
|
-
name: providers.tokenName(token),
|
|
1746
|
-
dependencies: providers.tokenDependencies(container, providers$1, token),
|
|
1747
|
-
...token === store && {
|
|
1748
|
-
cooldownStore: true
|
|
1708
|
+
// Bound before the app's classes, so a class that injects it gets this instance; filled once they are bound
|
|
1709
|
+
const appClasses = [];
|
|
1710
|
+
container.bind(providers.HandlerRegistry).toConstantValue(new providers.HandlerRegistry(appClasses, options.messages, ()=>options.i18n));
|
|
1711
|
+
container.bind(providers.ShardContext).toDynamicValue(()=>{
|
|
1712
|
+
const client = container.get(discord_js.Client);
|
|
1713
|
+
return new providers.ShardContext(client, (service, method, args)=>Reflect.get(client, providers.SHARD_CALL_KEY)(service, method, args));
|
|
1714
|
+
}).inSingletonScope();
|
|
1715
|
+
providers.bindsOwnToken(container, providers.HandlerRegistry);
|
|
1716
|
+
providers.bindsOwnToken(container, providers.ShardContext);
|
|
1717
|
+
container.bind(providers.COOLDOWN_POLICY).toConstantValue(providers.cooldownPolicyFrom(options));
|
|
1718
|
+
// A store of the app's own is resolved like a service, so it can inject its client
|
|
1719
|
+
if (options.cooldownStore) {
|
|
1720
|
+
bindDependencies(container, options.cooldownStore, providers$1);
|
|
1721
|
+
container.bind(controller_decorator.CooldownStore).toService(options.cooldownStore);
|
|
1722
|
+
} else {
|
|
1723
|
+
container.bind(controller_decorator.CooldownStore).toConstantValue(new controller_decorator.MemoryCooldownStore());
|
|
1724
|
+
}
|
|
1725
|
+
// A class that injects the token depends on the app's store, the one unit whose hooks run
|
|
1726
|
+
providers.bindsOwnToken(container, controller_decorator.CooldownStore, options.cooldownStore);
|
|
1727
|
+
// After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
|
|
1728
|
+
// a class token that is provided is not also bound as itself
|
|
1729
|
+
for (const [token, provider] of providers$1){
|
|
1730
|
+
// Bound later, or for each call, so not caught below; supported until 5.0 refuses it
|
|
1731
|
+
if (token === controller_decorator.ThemeCache || token === controller_decorator.ExecutionContext) {
|
|
1732
|
+
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.');
|
|
1749
1733
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
...new Set([
|
|
1754
|
-
...appClasses,
|
|
1755
|
-
...[
|
|
1756
|
-
...providers$1.keys()
|
|
1757
|
-
].filter(providers.isAppClassToken)
|
|
1758
|
-
])
|
|
1759
|
-
];
|
|
1760
|
-
// From another shard a call names its class, so with process sharding every name it can reach is one class's
|
|
1761
|
-
if (meocordConfig.sharding?.mode === 'process') {
|
|
1762
|
-
const names = new Set();
|
|
1763
|
-
for (const cls of callable){
|
|
1764
|
-
if (names.has(cls.name)) {
|
|
1765
|
-
throw controller_decorator.refuse(new Error(`${cls.name}: two classes have this name; with process sharding, ShardContext.call finds a class in another ` + 'shard by its name, so give each controller, service and provided class a distinct name.'));
|
|
1734
|
+
if (container.isBound(token)) {
|
|
1735
|
+
controller_decorator.startupError(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${providers.tokenName(token)}, which MeoCord binds itself.`));
|
|
1736
|
+
continue;
|
|
1766
1737
|
}
|
|
1767
|
-
|
|
1738
|
+
providers.bindProvider(container, provider, (cls)=>bindDependencies(container, cls, providers$1));
|
|
1768
1739
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
for (const cls of runs.classes){
|
|
1773
|
-
Reflect.defineMetadata(controller_decorator.META.container, container, cls);
|
|
1774
|
-
}
|
|
1775
|
-
providers.prepareHandlerStages(container, appClasses);
|
|
1776
|
-
providers.bindObservers(container, observers);
|
|
1777
|
-
// Run by start() before it logs in: what may inject a provided value is resolved once every factory
|
|
1778
|
-
// has made its value, including those that return a promise
|
|
1779
|
-
const logger = this.logger;
|
|
1780
|
-
const startup = async ()=>{
|
|
1781
|
-
try {
|
|
1782
|
-
await providers.resolveProviders(container, providers$1, order);
|
|
1783
|
-
} catch (error) {
|
|
1784
|
-
logger.error(`${error.message}. The bot cannot start without it.`);
|
|
1785
|
-
logger.debug('Provider failure:', error.cause);
|
|
1786
|
-
controller_decorator.markExplained(error);
|
|
1787
|
-
throw error;
|
|
1740
|
+
// Bind all controllers and their transitive dependencies
|
|
1741
|
+
for (const ctrl of options.controllers){
|
|
1742
|
+
bindDependencies(container, ctrl, providers$1);
|
|
1788
1743
|
}
|
|
1789
|
-
// The listed services are made now, so constructors that attach listeners or connect run before login
|
|
1790
1744
|
for (const svc of options.services ?? []){
|
|
1791
|
-
container
|
|
1745
|
+
bindDependencies(container, svc, providers$1);
|
|
1792
1746
|
}
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
1747
|
+
if (themeResolver) bindDependencies(container, themeResolver, providers$1);
|
|
1748
|
+
// Observers are services too: bound here so their lifecycle hooks run in dependency order
|
|
1749
|
+
const observers = providers.appObservers(target);
|
|
1750
|
+
for (const observer of observers)bindDependencies(container, observer, providers$1);
|
|
1751
|
+
// What the stages and presenter inject, bound now so a service among them is made as the bot comes online
|
|
1752
|
+
const stageServices = startupRoots.stageDependencies(runs, providers$1, options.presenter);
|
|
1753
|
+
for (const service of stageServices)bindDependencies(container, service, providers$1);
|
|
1754
|
+
// What follows walks what these bound, so a class refused here is not reported again as missing
|
|
1755
|
+
controller_decorator.stopOnStartupErrors();
|
|
1756
|
+
// Providers first, then the services, the controllers and the observers, each after what it depends on. The app's
|
|
1757
|
+
// own classes, which a class injecting CooldownStore does not make of the store
|
|
1758
|
+
const order = providers.resolutionOrder(container, providers$1, [
|
|
1759
|
+
...providers$1.keys(),
|
|
1760
|
+
...options.services ?? [],
|
|
1761
|
+
...themeResolver ? [
|
|
1762
|
+
themeResolver
|
|
1763
|
+
] : [],
|
|
1764
|
+
...options.controllers,
|
|
1765
|
+
...observers
|
|
1766
|
+
], {
|
|
1767
|
+
followOwnTokens: false
|
|
1768
|
+
});
|
|
1769
|
+
appClasses.push(...order.filter((token)=>{
|
|
1770
|
+
const provider = providers$1.get(token);
|
|
1771
|
+
return providers.isAppClassToken(token) && (!provider || providers.isClassProvider(provider) && provider.useClass === token);
|
|
1772
|
+
}));
|
|
1773
|
+
providers.assertProvided(container, providers$1, runs.classes, '@MeoCord({ providers })');
|
|
1774
|
+
// With what the stages inject among them, each after what it depends on, so their hooks run as a listed service's do
|
|
1775
|
+
const roots = stageServices.length > 0 ? providers.resolutionOrder(container, providers$1, [
|
|
1776
|
+
...order,
|
|
1777
|
+
...stageServices
|
|
1778
|
+
], {
|
|
1779
|
+
followOwnTokens: false
|
|
1780
|
+
}) : order;
|
|
1781
|
+
// The store first, after only what it injects: it is ready before anything a call reaches, and shuts down last
|
|
1782
|
+
const store = options.cooldownStore;
|
|
1783
|
+
const lifecycle = (store ? providers.resolutionOrder(container, providers$1, [
|
|
1784
|
+
store,
|
|
1785
|
+
...roots
|
|
1786
|
+
]) : roots).map((token)=>({
|
|
1787
|
+
token,
|
|
1788
|
+
name: providers.tokenName(token),
|
|
1789
|
+
dependencies: providers.tokenDependencies(container, providers$1, token),
|
|
1790
|
+
...token === store && {
|
|
1791
|
+
cooldownStore: true
|
|
1792
|
+
}
|
|
1793
|
+
}));
|
|
1794
|
+
// What ShardContext.call reaches: the app's classes, and the classes its providers stand in for
|
|
1795
|
+
const callable = [
|
|
1796
|
+
...new Set([
|
|
1797
|
+
...appClasses,
|
|
1798
|
+
...[
|
|
1799
|
+
...providers$1.keys()
|
|
1800
|
+
].filter(providers.isAppClassToken)
|
|
1801
|
+
])
|
|
1802
|
+
];
|
|
1803
|
+
// From another shard a call names its class, so with process sharding every name it can reach is one class's
|
|
1804
|
+
if (meocordConfig.sharding?.mode === 'process') {
|
|
1805
|
+
const names = new Set();
|
|
1806
|
+
for (const cls of callable){
|
|
1807
|
+
if (names.has(cls.name)) {
|
|
1808
|
+
controller_decorator.startupError(new Error(`${cls.name}: two classes have this name; with process sharding, ShardContext.call finds a class in another ` + 'shard by its name, so give each controller, service and provided class a distinct name.'));
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
names.add(cls.name);
|
|
1812
|
+
}
|
|
1795
1813
|
}
|
|
1796
|
-
providers.
|
|
1797
|
-
providers.
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1814
|
+
providers.prepareHandlerStages(container, appClasses);
|
|
1815
|
+
providers.bindObservers(container, observers);
|
|
1816
|
+
// Read here with the rest, so a pattern that cannot be read is reported with them, before the dispatcher warns
|
|
1817
|
+
providers.buildMessageRoutes(options.controllers, options.messages);
|
|
1818
|
+
// Every check has run, and touched only the container: from here the app is made, once none found an error
|
|
1819
|
+
controller_decorator.stopOnStartupErrors();
|
|
1820
|
+
const discordClient = container.get(discord_js.Client);
|
|
1821
|
+
Reflect.set(discordClient, providers.SHARD_CALL_KEY, providers.shardCallHandler(container, ()=>callable, 'this app'));
|
|
1822
|
+
// Stamp every class the app runs with the container, so @UseGuard resolves guards on a direct call to any of them
|
|
1823
|
+
for (const cls of runs.classes){
|
|
1824
|
+
Reflect.defineMetadata(controller_decorator.META.container, container, cls);
|
|
1825
|
+
}
|
|
1826
|
+
providers.recordInstanceContainers(container, runs.classes);
|
|
1827
|
+
providers.prepareThemes(container, appClasses);
|
|
1828
|
+
// Run by start() before it logs in: what may inject a provided value is resolved once every factory
|
|
1829
|
+
// has made its value, including those that return a promise
|
|
1830
|
+
const logger = this.logger;
|
|
1831
|
+
const startup = async (control)=>{
|
|
1832
|
+
try {
|
|
1833
|
+
await providers.resolveProviders(container, providers$1, order, control);
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
logger.error(`${error.message}. The bot cannot start without it.`);
|
|
1836
|
+
logger.debug('Provider failure:', error.cause);
|
|
1837
|
+
controller_decorator.markExplained(error);
|
|
1838
|
+
throw error;
|
|
1839
|
+
}
|
|
1840
|
+
// The listed services are made now, so constructors that attach listeners or connect run before login; a stop
|
|
1841
|
+
// meanwhile makes no more of them
|
|
1842
|
+
for (const svc of options.services ?? []){
|
|
1843
|
+
if (control.stopped()) return;
|
|
1844
|
+
container.get(svc);
|
|
1845
|
+
}
|
|
1846
|
+
if (control.stopped()) return;
|
|
1847
|
+
if (shard && container.get(controller_decorator.CooldownStore) instanceof controller_decorator.MemoryCooldownStore) {
|
|
1848
|
+
warnPerShardCooldowns(options.controllers, logger);
|
|
1849
|
+
}
|
|
1850
|
+
providers.bindAppPresenter(container, target, discordClient);
|
|
1851
|
+
providers.registerClientTheme(discordClient, container);
|
|
1852
|
+
controller_decorator.registerClientTranslator(discordClient, options.i18n);
|
|
1853
|
+
};
|
|
1854
|
+
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
|
|
1855
|
+
options.warnUnanswered ?? process.env.NODE_ENV === 'development', shard);
|
|
1856
|
+
});
|
|
1802
1857
|
}
|
|
1803
1858
|
}
|
|
1804
1859
|
MeoCordFactory.logger = new controller_decorator.Logger();
|