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.
Files changed (140) hide show
  1. package/CHANGELOG.md +362 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-XKMlcUZz.cjs} +669 -185
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-BZgJyfZS.cjs} +218 -75
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CgfPQrNl.cjs} +374 -132
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-BUAR1WYG.cjs} +21 -7
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +365 -310
  10. package/dist/cjs/decorator/index.cjs +85 -61
  11. package/dist/cjs/testing/index.cjs +2236 -496
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/app-template/README.md.template +3 -1
  14. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +8 -4
  15. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +2 -2
  16. package/dist/esm/bin/app-template/src/guards/owner.guard.spec.ts.template +8 -4
  17. package/dist/esm/bin/app-template/src/guards/owner.guard.ts.template +1 -1
  18. package/dist/esm/bin/app-template/vitest.config.ts.template +1 -1
  19. package/dist/esm/bin/app-template/vitest.setup.ts.template +7 -3
  20. package/dist/esm/bin/generator.js +4 -5
  21. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  22. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  23. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  24. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  25. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  26. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  27. package/dist/esm/bin/meocord.js +84 -25
  28. package/dist/esm/build/load-config.pre-entry.js +17 -4
  29. package/dist/esm/build/rsbuild-config.js +16 -12
  30. package/dist/esm/build/typed-externals.js +36 -0
  31. package/dist/esm/common/cooldown-store.js +10 -2
  32. package/dist/esm/common/decorator.js +6 -6
  33. package/dist/esm/common/deprecation.js +2 -2
  34. package/dist/esm/common/errors.js +27 -2
  35. package/dist/esm/common/execution-context.js +1 -1
  36. package/dist/esm/common/meocord-text.js +11 -8
  37. package/dist/esm/common/metadata.js +1 -1
  38. package/dist/esm/common/response/call-order.js +9 -4
  39. package/dist/esm/common/response/presenter.js +33 -6
  40. package/dist/esm/common/response/response-state.js +84 -10
  41. package/dist/esm/common/route.js +21 -5
  42. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  43. package/dist/esm/common/translate-error.js +1 -1
  44. package/dist/esm/common/translator.js +37 -6
  45. package/dist/esm/core/command-conflicts.js +34 -34
  46. package/dist/esm/core/command-registration.js +93 -24
  47. package/dist/esm/core/command-routes.js +2 -2
  48. package/dist/esm/core/component-routes.js +74 -31
  49. package/dist/esm/core/cooldown-runner.js +29 -8
  50. package/dist/esm/core/dispatcher.js +51 -7
  51. package/dist/esm/core/fallback.js +20 -11
  52. package/dist/esm/core/filter-runner.js +2 -2
  53. package/dist/esm/core/guard-runner.js +16 -3
  54. package/dist/esm/core/handler-input.js +4 -3
  55. package/dist/esm/core/handler-pipeline.js +27 -14
  56. package/dist/esm/core/handler-registry.js +1 -1
  57. package/dist/esm/core/interceptor-runner.js +2 -2
  58. package/dist/esm/core/lifecycle-hooks.js +134 -47
  59. package/dist/esm/core/meocord-factory.js +241 -179
  60. package/dist/esm/core/meocord.app.js +116 -75
  61. package/dist/esm/core/message-routes.js +4 -3
  62. package/dist/esm/core/observer-runner.js +4 -3
  63. package/dist/esm/core/providers.js +45 -23
  64. package/dist/esm/core/route-specificity.js +4 -4
  65. package/dist/esm/core/running-calls.js +43 -0
  66. package/dist/esm/core/scalar-types.js +15 -3
  67. package/dist/esm/core/shard-context.js +6 -7
  68. package/dist/esm/core/shard-manager.js +16 -10
  69. package/dist/esm/core/startup-roots.js +23 -4
  70. package/dist/esm/core/theme-resolvers.js +4 -1
  71. package/dist/esm/core/theme-runtime.js +2 -2
  72. package/dist/esm/core/theme-scope.js +3 -1
  73. package/dist/esm/core/theme-validation.js +17 -10
  74. package/dist/esm/decorator/app.decorator.js +12 -7
  75. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  76. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  77. package/dist/esm/decorator/controller.decorator.js +162 -83
  78. package/dist/esm/decorator/cooldown.decorator.js +20 -3
  79. package/dist/esm/decorator/defer.decorator.js +4 -4
  80. package/dist/esm/decorator/event.decorator.js +5 -5
  81. package/dist/esm/decorator/filter.decorator.js +7 -7
  82. package/dist/esm/decorator/guard.decorator.js +11 -10
  83. package/dist/esm/decorator/inject.decorator.js +4 -4
  84. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  85. package/dist/esm/decorator/observer.decorator.js +4 -4
  86. package/dist/esm/decorator/service.decorator.js +4 -4
  87. package/dist/esm/decorator/theme.decorator.js +3 -3
  88. package/dist/esm/decorator/validation.decorator.js +10 -10
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +27 -11
  91. package/dist/esm/testing/cooldown-store-suite.js +85 -15
  92. package/dist/esm/testing/discord-defaults.js +249 -21
  93. package/dist/esm/testing/index.js +4 -2
  94. package/dist/esm/testing/inspect-handler.js +20 -3
  95. package/dist/esm/testing/meocord-testing-module.js +352 -205
  96. package/dist/esm/testing/mock-fn.js +173 -22
  97. package/dist/esm/testing/mock-interaction.js +1214 -209
  98. package/dist/esm/testing/response.js +1 -1
  99. package/dist/esm/testing/routing.js +39 -14
  100. package/dist/esm/testing/snowflake.js +9 -2
  101. package/dist/esm/testing/startup-errors.js +26 -0
  102. package/dist/esm/testing/strict-mocks.js +65 -0
  103. package/dist/esm/testing/theme.js +1 -1
  104. package/dist/esm/util/bundle-entry.util.js +2 -1
  105. package/dist/esm/util/common.util.js +47 -19
  106. package/dist/esm/util/dev-runner.util.js +8 -8
  107. package/dist/esm/util/generator-cli.util.js +4 -5
  108. package/dist/esm/util/interaction.util.js +3 -3
  109. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  110. package/dist/esm/util/meocord-config-validation.util.js +4 -5
  111. package/dist/esm/util/meocord-source-config.util.js +65 -20
  112. package/dist/esm/util/metadata-keys.js +1 -0
  113. package/dist/esm/util/method-wrapper.util.js +16 -0
  114. package/dist/esm/util/platform.util.js +10 -10
  115. package/dist/esm/util/refusal.util.js +148 -8
  116. package/dist/esm/util/shard-process.util.js +4 -1
  117. package/dist/esm/util/sharding-mode.util.js +5 -7
  118. package/dist/esm/util/shared-state.util.js +21 -0
  119. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  120. package/dist/esm/util/tsconfig.util.js +10 -10
  121. package/dist/esm/util/value.util.js +12 -1
  122. package/dist/types/common/index.d.cts +8 -8
  123. package/dist/types/common/index.d.ts +8 -8
  124. package/dist/types/core/index.d.cts +10 -7
  125. package/dist/types/core/index.d.ts +10 -7
  126. package/dist/types/decorator/index.d.cts +31 -29
  127. package/dist/types/decorator/index.d.ts +31 -29
  128. package/dist/types/{index-D7n0FBMm.d.ts → index-DKJclIi-.d.ts} +107 -36
  129. package/dist/types/{index-DEP0NYrU.d.cts → index-Dlf0ZZtS.d.cts} +107 -36
  130. package/dist/types/interface/index.d.cts +1 -1
  131. package/dist/types/interface/index.d.ts +1 -1
  132. package/dist/types/{route-C-aMI7Sl.d.cts → route-Bh1U13yy.d.cts} +8 -4
  133. package/dist/types/{route-C-aMI7Sl.d.ts → route-Bh1U13yy.d.ts} +8 -4
  134. package/dist/types/testing/index.d.cts +406 -70
  135. package/dist/types/testing/index.d.ts +406 -70
  136. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  137. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  138. package/dist/types/{translator-J6tIUfdp.d.ts → translator-Bg7hwxDF.d.cts} +21 -8
  139. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-CeP4bCzp.d.ts} +21 -8
  140. package/package.json +4 -3
@@ -1,21 +1,21 @@
1
1
  'use strict';
2
2
 
3
3
  require('reflect-metadata');
4
- var providers = require('../_shared/providers-BMMLp2SN.cjs');
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-BywwkU9O.cjs');
7
+ var controller_decorator = require('../_shared/controller.decorator-XKMlcUZz.cjs');
8
8
  require('node:crypto');
9
- var shardedCooldownStore = require('../_shared/sharded-cooldown-store-B65iaiAy.cjs');
10
- var path$1 = require('path');
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-BnM8FbSH.cjs');
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. */ 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));
@@ -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)) throw controller_decorator.refuse(providers.singletonContextError(cls));
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)) throw controller_decorator.refuse(controller_decorator.missingTranslatorError(cls));
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' || 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,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, 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
- * patterns that match the same customIds. In a built application it is logged first, with where in the source it
1587
- * comes from, so `isExplainedError()` tells a caller not to log it again; in a shard of process sharding, the
1588
- * manager logs it.
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 (shardedCooldownStore.isShardProcess()) endFailedShard(error);
1595
- else if (controller_decorator.isBuiltApplication() && controller_decorator.isRefusal(error) && !controller_decorator.isExplainedError(error)) {
1596
- this.logger.error(controller_decorator.describeRefusal(error, process.cwd()));
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 options = Reflect.getMetadata(controller_decorator.META.appOptions, target);
1604
- if (!options) {
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
- startupRoots.assertDistinctCommands(options.controllers);
1613
- const providers$1 = providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
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 (!shardedCooldownStore.isShardProcess()) {
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
- // Every class the app runs, its stages and presenter included, checked before binding, where inversify would
1656
- // otherwise fail first with an error about compiler options, or a guard only at its first call
1657
- const stages = providers.appStages(target);
1658
- const runs = startupRoots.startupClasses({
1659
- controllers: options.controllers,
1660
- services: options.services ?? [],
1661
- providers: providers$1,
1662
- stages,
1663
- presenter: options.presenter,
1664
- cooldownStore: options.cooldownStore,
1665
- themeFor: options.themeFor,
1666
- observers: providers.appObservers(target)
1667
- });
1668
- startupRoots.assertStartupClasses(runs, {
1669
- translator: options.i18n !== undefined || providers$1.has(controller_decorator.Translator)
1670
- });
1671
- const container = new inversify.Container();
1672
- const translator = options.i18n !== undefined || providers$1.has(controller_decorator.Translator);
1673
- providers.markStartupChecked(container, [
1674
- ...runs.classes,
1675
- ...providers.meocordClasses()
1676
- ], startupRoots.lateGuardCheck(providers$1, translator));
1677
- providers.bindGlobalStages(container, stages);
1678
- // Bind the Discord client as a constant value
1679
- const discordClient = new discord_js.Client(shardedCooldownStore.clientOptionsWithSharding(this.effectiveConfig(meocordConfig), options.clientOptions));
1680
- container.bind(discord_js.Client).toConstantValue(discordClient);
1681
- if (options.i18n) {
1682
- container.bind(controller_decorator.Translator).toConstantValue(options.i18n);
1683
- providers.bindsOwnToken(container, controller_decorator.Translator);
1684
- }
1685
- // Bound before the app's classes, so a class that injects it gets this instance; filled once they are bound
1686
- const appClasses = [];
1687
- container.bind(providers.HandlerRegistry).toConstantValue(new providers.HandlerRegistry(appClasses, options.messages, ()=>options.i18n));
1688
- container.bind(providers.ShardContext).toConstantValue(new providers.ShardContext(discordClient, (service, method, args)=>Reflect.get(discordClient, providers.SHARD_CALL_KEY)(service, method, args)));
1689
- providers.bindsOwnToken(container, providers.HandlerRegistry);
1690
- providers.bindsOwnToken(container, providers.ShardContext);
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
- providers.bindProvider(container, provider, (cls)=>bindDependencies(container, cls, providers$1));
1708
- }
1709
- // Bind all controllers and their transitive dependencies
1710
- for (const ctrl of options.controllers){
1711
- bindDependencies(container, ctrl, providers$1);
1712
- }
1713
- for (const svc of options.services ?? []){
1714
- bindDependencies(container, svc, providers$1);
1715
- }
1716
- if (themeResolver) bindDependencies(container, themeResolver, providers$1);
1717
- // Observers are services too: bound here so their lifecycle hooks run in dependency order
1718
- const observers = providers.appObservers(target);
1719
- for (const observer of observers)bindDependencies(container, observer, providers$1);
1720
- // Providers first, then the services, the controllers and the observers, each after what it depends on. The app's
1721
- // own classes, which a class injecting CooldownStore does not make of the store
1722
- const order = providers.resolutionOrder(container, providers$1, [
1723
- ...providers$1.keys(),
1724
- ...options.services ?? [],
1725
- ...themeResolver ? [
1726
- themeResolver
1727
- ] : [],
1728
- ...options.controllers,
1729
- ...observers
1730
- ], {
1731
- followOwnTokens: false
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
- // What ShardContext.call reaches: the app's classes, and the classes its providers stand in for
1752
- const callable = [
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
- names.add(cls.name);
1738
+ providers.bindProvider(container, provider, (cls)=>bindDependencies(container, cls, providers$1));
1768
1739
  }
1769
- }
1770
- Reflect.set(discordClient, providers.SHARD_CALL_KEY, providers.shardCallHandler(container, ()=>callable, 'this app'));
1771
- // Stamp every class the app runs with the container, so @UseGuard resolves guards on a direct call to any of them
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.get(svc);
1745
+ bindDependencies(container, svc, providers$1);
1792
1746
  }
1793
- if (shardedCooldownStore.shardingRole(meocordConfig) === 'shard' && container.get(controller_decorator.CooldownStore) instanceof controller_decorator.MemoryCooldownStore) {
1794
- warnPerShardCooldowns(options.controllers, logger);
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.bindAppPresenter(container, target, discordClient);
1797
- providers.registerClientTheme(discordClient, container);
1798
- controller_decorator.registerClientTranslator(discordClient, options.i18n);
1799
- };
1800
- 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
1801
- options.warnUnanswered ?? process.env.NODE_ENV === 'development');
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();