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.
Files changed (103) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
  4. package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
  5. package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
  7. package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
  8. package/dist/cjs/common/index.cjs +3 -3
  9. package/dist/cjs/core/index.cjs +188 -169
  10. package/dist/cjs/decorator/index.cjs +52 -7
  11. package/dist/cjs/testing/index.cjs +1854 -311
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/generator.js +4 -5
  14. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  15. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  16. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  17. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  18. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  19. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  20. package/dist/esm/bin/meocord.js +84 -25
  21. package/dist/esm/build/load-config.pre-entry.js +13 -3
  22. package/dist/esm/build/rsbuild-config.js +16 -12
  23. package/dist/esm/build/typed-externals.js +36 -0
  24. package/dist/esm/common/cooldown-store.js +10 -2
  25. package/dist/esm/common/errors.js +25 -0
  26. package/dist/esm/common/meocord-text.js +11 -8
  27. package/dist/esm/common/response/call-order.js +9 -4
  28. package/dist/esm/common/response/presenter.js +33 -6
  29. package/dist/esm/common/response/response-state.js +89 -15
  30. package/dist/esm/common/route.js +11 -4
  31. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  32. package/dist/esm/common/translator.js +52 -11
  33. package/dist/esm/core/command-registration.js +93 -24
  34. package/dist/esm/core/command-routes.js +2 -2
  35. package/dist/esm/core/cooldown-runner.js +29 -8
  36. package/dist/esm/core/dispatcher.js +9 -8
  37. package/dist/esm/core/fallback.js +20 -11
  38. package/dist/esm/core/filter-runner.js +2 -2
  39. package/dist/esm/core/guard-runner.js +16 -3
  40. package/dist/esm/core/handler-input.js +4 -3
  41. package/dist/esm/core/handler-pipeline.js +10 -4
  42. package/dist/esm/core/lifecycle-hooks.js +134 -47
  43. package/dist/esm/core/meocord-factory.js +59 -34
  44. package/dist/esm/core/meocord.app.js +116 -75
  45. package/dist/esm/core/message-help.js +32 -6
  46. package/dist/esm/core/observer-runner.js +2 -1
  47. package/dist/esm/core/providers.js +19 -4
  48. package/dist/esm/core/running-calls.js +43 -0
  49. package/dist/esm/core/shard-context.js +5 -6
  50. package/dist/esm/core/shard-manager.js +16 -10
  51. package/dist/esm/core/stage-scope.js +2 -1
  52. package/dist/esm/core/startup-roots.js +21 -1
  53. package/dist/esm/core/theme-resolvers.js +4 -1
  54. package/dist/esm/core/theme-scope.js +7 -2
  55. package/dist/esm/core/theme-validation.js +17 -10
  56. package/dist/esm/decorator/app.decorator.js +31 -0
  57. package/dist/esm/decorator/controller.decorator.js +56 -20
  58. package/dist/esm/decorator/cooldown.decorator.js +17 -0
  59. package/dist/esm/decorator/filter.decorator.js +2 -2
  60. package/dist/esm/decorator/guard.decorator.js +3 -2
  61. package/dist/esm/package.json.js +1 -1
  62. package/dist/esm/testing/catalog.js +20 -13
  63. package/dist/esm/testing/cooldown-store-suite.js +67 -5
  64. package/dist/esm/testing/discord-defaults.js +225 -30
  65. package/dist/esm/testing/meocord-testing-module.js +158 -44
  66. package/dist/esm/testing/mock-fn.js +107 -26
  67. package/dist/esm/testing/mock-interaction.js +1250 -182
  68. package/dist/esm/testing/snowflake.js +9 -2
  69. package/dist/esm/testing/strict-mocks.js +40 -17
  70. package/dist/esm/util/common.util.js +47 -19
  71. package/dist/esm/util/dev-runner.util.js +8 -8
  72. package/dist/esm/util/generator-cli.util.js +4 -5
  73. package/dist/esm/util/interaction.util.js +2 -2
  74. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  75. package/dist/esm/util/meocord-config-validation.util.js +3 -5
  76. package/dist/esm/util/meocord-source-config.util.js +65 -20
  77. package/dist/esm/util/method-wrapper.util.js +16 -0
  78. package/dist/esm/util/platform.util.js +10 -10
  79. package/dist/esm/util/shard-process.util.js +4 -1
  80. package/dist/esm/util/sharding-mode.util.js +5 -7
  81. package/dist/esm/util/shared-state.util.js +21 -0
  82. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  83. package/dist/esm/util/tsconfig.util.js +10 -10
  84. package/dist/esm/util/value.util.js +30 -2
  85. package/dist/types/common/index.d.cts +10 -7
  86. package/dist/types/common/index.d.ts +10 -7
  87. package/dist/types/core/index.d.cts +3 -2
  88. package/dist/types/core/index.d.ts +3 -2
  89. package/dist/types/decorator/index.d.cts +1 -0
  90. package/dist/types/decorator/index.d.ts +1 -0
  91. package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
  92. package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
  93. package/dist/types/interface/index.d.cts +1 -1
  94. package/dist/types/interface/index.d.ts +1 -1
  95. package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
  96. package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
  97. package/dist/types/testing/index.d.cts +198 -62
  98. package/dist/types/testing/index.d.ts +198 -62
  99. package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  100. package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  101. package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
  102. package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
  103. package/package.json +2 -2
@@ -1,21 +1,21 @@
1
1
  'use strict';
2
2
 
3
3
  require('reflect-metadata');
4
- var providers = require('../_shared/providers-ChvMqnLd.cjs');
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-CRn9kWob.cjs');
7
+ var controller_decorator = require('../_shared/controller.decorator-U2NBWcoU.cjs');
8
8
  require('node:crypto');
9
- var shardedCooldownStore = require('../_shared/sharded-cooldown-store-BdAUTdYs.cjs');
10
- var path$1 = require('path');
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-DhDVbT5M.cjs');
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. */ function onDevRunnerStop(stop) {
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 && !shardedCooldownStore.isShardProcess();
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' || shardedCooldownStore.isShardProcess()) return;
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 (shardedCooldownStore.isShardProcess()) {
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 (shardedCooldownStore.isShardProcess()) await tellManager({
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 handled = (async ()=>{
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 the cooldown store outlasts at shutdown; the ready listener's work is not one
702
- if (event !== 'clientReady') {
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 (shardedCooldownStore.isShardProcess() && !this.closing) endFailedShard(error);
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
- if (shardedCooldownStore.isShardProcess()) await tellManager({
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 (shardedCooldownStore.isShardProcess() && shardedCooldownStore.managerGone()) void shutdownAndExit();
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
- await this.prepare();
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 nothing to close: nothing is attached or logged in
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 (!shardedCooldownStore.isShardProcess()) await this.registerCommands();
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 (!shardedCooldownStore.isShardProcess()) this.logger.error(explanation);
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 listener = async (...args)=>{
953
- try {
954
- const instance = this.container.get(lifecycleClass);
955
- await providers.runHandler(this.container, instance, method, args, {
956
- fallback,
957
- type: 'event'
958
- });
959
- } catch (error) {
960
- // Only resolving the instance can fail here; the pipeline hands every other error to the fallback
961
- logError(error);
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
- primary: client.shard ? client.shard.ids.includes(0) : true
1034
- }, entries, {
1035
- resolveFailed: (unit, error)=>this.logger.error(`Could not resolve ${unit.name} to run its lifecycle hooks:`, error),
1036
- hookFailed: (unit, error)=>this.logger.error(`onReady failed in ${unit.name}:`, error),
1037
- dependsOnFailed: (unit, failed)=>this.logger.warn(`Running onReady in ${unit.name} although it depends on ${failed.map(({ name })=>name).join(', ')}, which failed.`),
1038
- slow: (unit)=>this.logger.warn(`onReady in ${unit.name} has run for over ${SLOW_READY_HOOK_MS} ms; the hooks after it are waiting.`)
1039
- }, // Shutdown has begun: the client is going away, so no further hook starts
1040
- {
1041
- stopped: ()=>this.closing,
1042
- slowAfterMs: SLOW_READY_HOOK_MS,
1043
- settled: (unit)=>unit.cooldownStore && this.storeReady?.resolve()
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(entries) {
1049
- await startupRoots.runShutdownSequence(this.container, entries, {
1050
- // No new call starts once the listeners are gone
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.removeAllListeners();
1053
- return startupRoots.callsSettled(this.calls);
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
- // A login that failed never ran onReady, so there is nothing for onShutdown to undo
1085
- if (this.lifecycleEntries) await this.runShutdownHooks(this.lifecycleEntries);
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, each settling once its handling has. */ this.calls = new Set();
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 (!underDevRunner()) return this.exit(1);
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' || shardedCooldownStore.isShardProcess() || shardedCooldownStore.processShardingEnabled(config)) return config;
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, when two handlers take one command, or two builder
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 (shardedCooldownStore.isShardProcess()) endFailedShard(error);
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 options = Reflect.getMetadata(controller_decorator.META.appOptions, target);
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
- ...options?.controllers ?? []
1608
+ ...appOptions?.controllers ?? []
1614
1609
  ]);
1615
- if (!options) {
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
- return providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
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 (!shardedCooldownStore.isShardProcess()) {
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
- ...order
1773
- ]) : order).map((token)=>({
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 (shardedCooldownStore.shardingRole(meocordConfig) === 'shard' && container.get(controller_decorator.CooldownStore) instanceof controller_decorator.MemoryCooldownStore) {
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
  }