meocord 4.1.1 → 4.2.0

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 (96) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +23 -23
  3. package/dist/cjs/_shared/{controller.decorator-BywwkU9O.cjs → controller.decorator-CRn9kWob.cjs} +295 -87
  4. package/dist/cjs/_shared/{providers-BMMLp2SN.cjs → providers-ChvMqnLd.cjs} +74 -40
  5. package/dist/cjs/_shared/{sharded-cooldown-store-B65iaiAy.cjs → sharded-cooldown-store-BdAUTdYs.cjs} +1 -1
  6. package/dist/cjs/_shared/{startup-roots-lFc69AxC.cjs → startup-roots-CQo8mSP8.cjs} +158 -72
  7. package/dist/cjs/_shared/{translate-error-BnM8FbSH.cjs → translate-error-DhDVbT5M.cjs} +12 -3
  8. package/dist/cjs/common/index.cjs +4 -4
  9. package/dist/cjs/core/index.cjs +195 -157
  10. package/dist/cjs/decorator/index.cjs +66 -57
  11. package/dist/cjs/testing/index.cjs +736 -307
  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/build/load-config.pre-entry.js +4 -1
  21. package/dist/esm/common/decorator.js +6 -6
  22. package/dist/esm/common/deprecation.js +2 -2
  23. package/dist/esm/common/errors.js +2 -2
  24. package/dist/esm/common/execution-context.js +1 -1
  25. package/dist/esm/common/metadata.js +1 -1
  26. package/dist/esm/common/route.js +10 -1
  27. package/dist/esm/common/translate-error.js +1 -1
  28. package/dist/esm/core/command-conflicts.js +34 -34
  29. package/dist/esm/core/component-routes.js +74 -31
  30. package/dist/esm/core/dispatcher.js +51 -7
  31. package/dist/esm/core/filter-runner.js +2 -2
  32. package/dist/esm/core/handler-pipeline.js +27 -14
  33. package/dist/esm/core/handler-registry.js +1 -1
  34. package/dist/esm/core/interceptor-runner.js +2 -2
  35. package/dist/esm/core/meocord-factory.js +196 -157
  36. package/dist/esm/core/message-routes.js +4 -3
  37. package/dist/esm/core/observer-runner.js +4 -3
  38. package/dist/esm/core/providers.js +38 -21
  39. package/dist/esm/core/route-specificity.js +4 -4
  40. package/dist/esm/core/scalar-types.js +15 -3
  41. package/dist/esm/core/shard-context.js +1 -1
  42. package/dist/esm/core/startup-roots.js +2 -3
  43. package/dist/esm/core/theme-runtime.js +2 -2
  44. package/dist/esm/decorator/app.decorator.js +12 -7
  45. package/dist/esm/decorator/command-builder.decorator.js +4 -4
  46. package/dist/esm/decorator/controller-class.decorator.js +9 -4
  47. package/dist/esm/decorator/controller.decorator.js +110 -67
  48. package/dist/esm/decorator/cooldown.decorator.js +3 -3
  49. package/dist/esm/decorator/defer.decorator.js +4 -4
  50. package/dist/esm/decorator/event.decorator.js +5 -5
  51. package/dist/esm/decorator/filter.decorator.js +7 -7
  52. package/dist/esm/decorator/guard.decorator.js +8 -8
  53. package/dist/esm/decorator/inject.decorator.js +4 -4
  54. package/dist/esm/decorator/interceptor.decorator.js +7 -7
  55. package/dist/esm/decorator/observer.decorator.js +4 -4
  56. package/dist/esm/decorator/service.decorator.js +4 -4
  57. package/dist/esm/decorator/theme.decorator.js +3 -3
  58. package/dist/esm/decorator/validation.decorator.js +10 -10
  59. package/dist/esm/package.json.js +1 -1
  60. package/dist/esm/testing/catalog.js +1 -1
  61. package/dist/esm/testing/cooldown-store-suite.js +18 -10
  62. package/dist/esm/testing/discord-defaults.js +60 -7
  63. package/dist/esm/testing/index.js +4 -2
  64. package/dist/esm/testing/inspect-handler.js +20 -3
  65. package/dist/esm/testing/meocord-testing-module.js +229 -188
  66. package/dist/esm/testing/mock-fn.js +80 -10
  67. package/dist/esm/testing/mock-interaction.js +226 -78
  68. package/dist/esm/testing/response.js +1 -1
  69. package/dist/esm/testing/routing.js +39 -14
  70. package/dist/esm/testing/startup-errors.js +26 -0
  71. package/dist/esm/testing/strict-mocks.js +45 -0
  72. package/dist/esm/testing/theme.js +1 -1
  73. package/dist/esm/util/bundle-entry.util.js +2 -1
  74. package/dist/esm/util/interaction.util.js +1 -1
  75. package/dist/esm/util/meocord-config-validation.util.js +1 -0
  76. package/dist/esm/util/metadata-keys.js +1 -0
  77. package/dist/esm/util/refusal.util.js +148 -8
  78. package/dist/types/common/index.d.cts +8 -8
  79. package/dist/types/common/index.d.ts +8 -8
  80. package/dist/types/core/index.d.cts +8 -6
  81. package/dist/types/core/index.d.ts +8 -6
  82. package/dist/types/decorator/index.d.cts +30 -29
  83. package/dist/types/decorator/index.d.ts +30 -29
  84. package/dist/types/{index-D7n0FBMm.d.ts → index-CdXsFgjE.d.ts} +62 -15
  85. package/dist/types/{index-DEP0NYrU.d.cts → index-nn5p7OoX.d.cts} +62 -15
  86. package/dist/types/interface/index.d.cts +1 -1
  87. package/dist/types/interface/index.d.ts +1 -1
  88. package/dist/types/{route-C-aMI7Sl.d.cts → route-BZgFoNeB.d.cts} +5 -2
  89. package/dist/types/{route-C-aMI7Sl.d.ts → route-BZgFoNeB.d.ts} +5 -2
  90. package/dist/types/testing/index.d.cts +245 -31
  91. package/dist/types/testing/index.d.ts +245 -31
  92. package/dist/types/{theme-resolvers-DedxyscT.d.ts → theme-resolvers-BcOJVzR9.d.ts} +1 -1
  93. package/dist/types/{theme-resolvers-COHdX5g6.d.cts → theme-resolvers-UKr3-6Ys.d.cts} +1 -1
  94. package/dist/types/{translator-J6tIUfdp.d.ts → translator-BViWKtLJ.d.ts} +1 -1
  95. package/dist/types/{translator-B4mc1lzJ.d.cts → translator-DHMkkCpg.d.cts} +1 -1
  96. package/package.json +3 -2
@@ -1,14 +1,14 @@
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-ChvMqnLd.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-CRn9kWob.cjs');
8
8
  require('node:crypto');
9
- var shardedCooldownStore = require('../_shared/sharded-cooldown-store-B65iaiAy.cjs');
9
+ var shardedCooldownStore = require('../_shared/sharded-cooldown-store-BdAUTdYs.cjs');
10
10
  var path$1 = require('path');
11
- var startupRoots = require('../_shared/startup-roots-lFc69AxC.cjs');
11
+ var startupRoots = require('../_shared/startup-roots-CQo8mSP8.cjs');
12
12
  var enum_index = require('../enum/index.cjs');
13
13
  var node_fs = require('node:fs');
14
14
  var path = require('node:path');
@@ -23,7 +23,7 @@ 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-DhDVbT5M.cjs');
27
27
 
28
28
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
29
29
 
@@ -1510,14 +1510,14 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
1510
1510
  */ function bindDependencies(container, cls, providers$1) {
1511
1511
  // A provided class is bound by its own provider, wherever in the list that provider comes
1512
1512
  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));
1513
+ if (providers.injectedTokens(cls).includes(controller_decorator.ExecutionContext)) return controller_decorator.startupError(providers.singletonContextError(cls));
1514
1514
  providers.assertStartupChecked(container, cls);
1515
1515
  providers.makeInjectable(cls);
1516
1516
  container.bind(cls).toSelf().inSingletonScope();
1517
1517
  // By constructor type or @inject token; an interface-typed parameter records Object, which is skipped
1518
1518
  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);
1519
+ if (dep === controller_decorator.Translator && !container.isBound(controller_decorator.Translator)) controller_decorator.startupError(controller_decorator.missingTranslatorError(cls));
1520
+ else if (providers.isAppClassToken(dep)) bindDependencies(container, dep, providers$1);
1521
1521
  }
1522
1522
  }
1523
1523
  /**
@@ -1583,34 +1583,51 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
1583
1583
  * load, naming the file and the reason, with what to do about it, when a provider cannot
1584
1584
  * be bound, such as one for a token MeoCord binds itself, when two handlers take one command, or two builder
1585
1585
  * 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.
1586
+ * patterns that match the same customIds. Each check reports every error it finds, and the first is thrown,
1587
+ * unchanged. When there are several, each is logged first, with where in the source it comes from; a lone one is
1588
+ * logged in a built application. Either way `isExplainedError()` then tells a caller not to log it again; in a
1589
+ * shard of process sharding, the manager logs it. With `startupErrors: 'all'` in `meocord.config.ts`, the errors
1590
+ * decorators kept are among them.
1589
1591
  */ static create(target) {
1590
1592
  try {
1591
1593
  return this.createApplication(target);
1592
1594
  } catch (error) {
1593
1595
  // A shard ends, so its manager restarts it, or stops every shard for a refusal, which it logs
1594
1596
  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);
1597
+ else if (controller_decorator.isRefusal(error) && !controller_decorator.isExplainedError(error)) {
1598
+ // Every startup error found together is logged, in a test too; a lone one is logged in a built application
1599
+ if (controller_decorator.logStartupErrors(error, (text)=>this.logger.error(text), 'the bot did not start')) controller_decorator.markExplained(error);
1600
+ else if (controller_decorator.isBuiltApplication()) {
1601
+ this.logger.error(controller_decorator.describeRefusal(error, process.cwd()));
1602
+ controller_decorator.markExplained(error);
1603
+ }
1598
1604
  }
1599
1605
  throw error;
1600
1606
  }
1601
1607
  }
1602
1608
  static createApplication(target) {
1603
1609
  const options = Reflect.getMetadata(controller_decorator.META.appOptions, target);
1610
+ // With startupErrors: 'all', what the decorators kept: a built bot's every one, else the app's and its controllers'
1611
+ const declared = controller_decorator.isBuiltApplication() ? controller_decorator.allDeclaredErrors() : controller_decorator.declaredErrorsOf([
1612
+ target,
1613
+ ...options?.controllers ?? []
1614
+ ]);
1604
1615
  if (!options) {
1616
+ // A refused @MeoCord stores no options, so its own errors say why
1617
+ controller_decorator.collectStartupErrors(()=>declared.forEach(controller_decorator.startupError));
1605
1618
  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
1619
  }
1607
1620
  const meocordConfig = controller_decorator.loadMeoCordConfig();
1608
1621
  if (!meocordConfig) {
1609
1622
  throw controller_decorator.refuse(new Error(controller_decorator.compiledConfigMessage()));
1610
1623
  }
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 })');
1624
+ // Before any of the three ways a bot runs, so none registers or dispatches a command only one handler could take; every
1625
+ // error each check finds is reported, the first thrown
1626
+ const providers$1 = controller_decorator.collectStartupErrors(()=>{
1627
+ declared.forEach(controller_decorator.startupError);
1628
+ startupRoots.assertDistinctCommands(options.controllers);
1629
+ return providers.providerMap(options.providers ?? [], '@MeoCord({ providers })');
1630
+ });
1614
1631
  // A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
1615
1632
  const themeResolver = controller_decorator.themeResolverClass(options.themeFor);
1616
1633
  // The app's classes and every class they inject, which the container binds
@@ -1652,153 +1669,174 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
1652
1669
  config: meocordConfig
1653
1670
  });
1654
1671
  }
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.`));
1672
+ // The checks of what the app binds and runs report every error each finds, and the first is thrown
1673
+ return controller_decorator.collectStartupErrors(()=>{
1674
+ // Every class the app runs, its stages and presenter included, checked before binding, where inversify would
1675
+ // otherwise fail first with an error about compiler options, or a guard only at its first call
1676
+ const stages = providers.appStages(target);
1677
+ const runs = startupRoots.startupClasses({
1678
+ controllers: options.controllers,
1679
+ services: options.services ?? [],
1680
+ providers: providers$1,
1681
+ stages,
1682
+ presenter: options.presenter,
1683
+ cooldownStore: options.cooldownStore,
1684
+ themeFor: options.themeFor,
1685
+ observers: providers.appObservers(target)
1686
+ });
1687
+ startupRoots.assertStartupClasses(runs, {
1688
+ translator: options.i18n !== undefined || providers$1.has(controller_decorator.Translator)
1689
+ });
1690
+ // Outside a built application, the kept errors of the stages and other classes the app's classes reach
1691
+ if (!controller_decorator.isBuiltApplication()) controller_decorator.declaredErrorsOf(runs.classes).forEach(controller_decorator.startupError);
1692
+ // The checks of the wiring below run on classes that passed these, so a mistake in one is not reported again there
1693
+ controller_decorator.stopOnStartupErrors();
1694
+ // The checks bind into this container and change nothing else: what they bind is made once all have passed
1695
+ const container = new inversify.Container();
1696
+ const translator = options.i18n !== undefined || providers$1.has(controller_decorator.Translator);
1697
+ providers.markStartupChecked(container, [
1698
+ ...runs.classes,
1699
+ ...providers.meocordClasses()
1700
+ ], startupRoots.lateGuardCheck(providers$1, translator));
1701
+ providers.bindGlobalStages(container, stages);
1702
+ // 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
+ container.bind(discord_js.Client).toDynamicValue(()=>new discord_js.Client(clientOptions)).inSingletonScope();
1705
+ if (options.i18n) {
1706
+ container.bind(controller_decorator.Translator).toConstantValue(options.i18n);
1707
+ providers.bindsOwnToken(container, controller_decorator.Translator);
1706
1708
  }
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
1749
- }
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.'));
1709
+ // Bound before the app's classes, so a class that injects it gets this instance; filled once they are bound
1710
+ const appClasses = [];
1711
+ container.bind(providers.HandlerRegistry).toConstantValue(new providers.HandlerRegistry(appClasses, options.messages, ()=>options.i18n));
1712
+ container.bind(providers.ShardContext).toDynamicValue(()=>{
1713
+ const client = container.get(discord_js.Client);
1714
+ return new providers.ShardContext(client, (service, method, args)=>Reflect.get(client, providers.SHARD_CALL_KEY)(service, method, args));
1715
+ }).inSingletonScope();
1716
+ providers.bindsOwnToken(container, providers.HandlerRegistry);
1717
+ providers.bindsOwnToken(container, providers.ShardContext);
1718
+ container.bind(providers.COOLDOWN_POLICY).toConstantValue(providers.cooldownPolicyFrom(options));
1719
+ // A store of the app's own is resolved like a service, so it can inject its client
1720
+ if (options.cooldownStore) {
1721
+ bindDependencies(container, options.cooldownStore, providers$1);
1722
+ container.bind(controller_decorator.CooldownStore).toService(options.cooldownStore);
1723
+ } else {
1724
+ container.bind(controller_decorator.CooldownStore).toConstantValue(new controller_decorator.MemoryCooldownStore());
1725
+ }
1726
+ // A class that injects the token depends on the app's store, the one unit whose hooks run
1727
+ providers.bindsOwnToken(container, controller_decorator.CooldownStore, options.cooldownStore);
1728
+ // After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
1729
+ // a class token that is provided is not also bound as itself
1730
+ for (const [token, provider] of providers$1){
1731
+ if (container.isBound(token)) {
1732
+ controller_decorator.startupError(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${providers.tokenName(token)}, which MeoCord binds itself.`));
1733
+ continue;
1766
1734
  }
1767
- names.add(cls.name);
1735
+ providers.bindProvider(container, provider, (cls)=>bindDependencies(container, cls, providers$1));
1768
1736
  }
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;
1737
+ // Bind all controllers and their transitive dependencies
1738
+ for (const ctrl of options.controllers){
1739
+ bindDependencies(container, ctrl, providers$1);
1788
1740
  }
1789
- // The listed services are made now, so constructors that attach listeners or connect run before login
1790
1741
  for (const svc of options.services ?? []){
1791
- container.get(svc);
1742
+ bindDependencies(container, svc, providers$1);
1743
+ }
1744
+ if (themeResolver) bindDependencies(container, themeResolver, providers$1);
1745
+ // Observers are services too: bound here so their lifecycle hooks run in dependency order
1746
+ const observers = providers.appObservers(target);
1747
+ for (const observer of observers)bindDependencies(container, observer, providers$1);
1748
+ // What follows walks what these bound, so a class refused here is not reported again as missing
1749
+ controller_decorator.stopOnStartupErrors();
1750
+ // Providers first, then the services, the controllers and the observers, each after what it depends on. The app's
1751
+ // own classes, which a class injecting CooldownStore does not make of the store
1752
+ const order = providers.resolutionOrder(container, providers$1, [
1753
+ ...providers$1.keys(),
1754
+ ...options.services ?? [],
1755
+ ...themeResolver ? [
1756
+ themeResolver
1757
+ ] : [],
1758
+ ...options.controllers,
1759
+ ...observers
1760
+ ], {
1761
+ followOwnTokens: false
1762
+ });
1763
+ appClasses.push(...order.filter((token)=>{
1764
+ const provider = providers$1.get(token);
1765
+ return providers.isAppClassToken(token) && (!provider || providers.isClassProvider(provider) && provider.useClass === token);
1766
+ }));
1767
+ providers.assertProvided(container, providers$1, runs.classes, '@MeoCord({ providers })');
1768
+ // The store first, after only what it injects: it is ready before anything a call reaches, and shuts down last
1769
+ const store = options.cooldownStore;
1770
+ const lifecycle = (store ? providers.resolutionOrder(container, providers$1, [
1771
+ store,
1772
+ ...order
1773
+ ]) : order).map((token)=>({
1774
+ token,
1775
+ name: providers.tokenName(token),
1776
+ dependencies: providers.tokenDependencies(container, providers$1, token),
1777
+ ...token === store && {
1778
+ cooldownStore: true
1779
+ }
1780
+ }));
1781
+ // What ShardContext.call reaches: the app's classes, and the classes its providers stand in for
1782
+ const callable = [
1783
+ ...new Set([
1784
+ ...appClasses,
1785
+ ...[
1786
+ ...providers$1.keys()
1787
+ ].filter(providers.isAppClassToken)
1788
+ ])
1789
+ ];
1790
+ // From another shard a call names its class, so with process sharding every name it can reach is one class's
1791
+ if (meocordConfig.sharding?.mode === 'process') {
1792
+ const names = new Set();
1793
+ for (const cls of callable){
1794
+ if (names.has(cls.name)) {
1795
+ 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.'));
1796
+ continue;
1797
+ }
1798
+ names.add(cls.name);
1799
+ }
1792
1800
  }
1793
- if (shardedCooldownStore.shardingRole(meocordConfig) === 'shard' && container.get(controller_decorator.CooldownStore) instanceof controller_decorator.MemoryCooldownStore) {
1794
- warnPerShardCooldowns(options.controllers, logger);
1801
+ providers.prepareHandlerStages(container, appClasses);
1802
+ providers.bindObservers(container, observers);
1803
+ // Read here with the rest, so a pattern that cannot be read is reported with them, before the dispatcher warns
1804
+ providers.buildMessageRoutes(options.controllers, options.messages);
1805
+ // Every check has run, and touched only the container: from here the app is made, once none found an error
1806
+ controller_decorator.stopOnStartupErrors();
1807
+ const discordClient = container.get(discord_js.Client);
1808
+ Reflect.set(discordClient, providers.SHARD_CALL_KEY, providers.shardCallHandler(container, ()=>callable, 'this app'));
1809
+ // Stamp every class the app runs with the container, so @UseGuard resolves guards on a direct call to any of them
1810
+ for (const cls of runs.classes){
1811
+ Reflect.defineMetadata(controller_decorator.META.container, container, cls);
1795
1812
  }
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');
1813
+ providers.prepareThemes(container, appClasses);
1814
+ // Run by start() before it logs in: what may inject a provided value is resolved once every factory
1815
+ // has made its value, including those that return a promise
1816
+ const logger = this.logger;
1817
+ const startup = async ()=>{
1818
+ try {
1819
+ await providers.resolveProviders(container, providers$1, order);
1820
+ } catch (error) {
1821
+ logger.error(`${error.message}. The bot cannot start without it.`);
1822
+ logger.debug('Provider failure:', error.cause);
1823
+ controller_decorator.markExplained(error);
1824
+ throw error;
1825
+ }
1826
+ // The listed services are made now, so constructors that attach listeners or connect run before login
1827
+ for (const svc of options.services ?? []){
1828
+ container.get(svc);
1829
+ }
1830
+ if (shardedCooldownStore.shardingRole(meocordConfig) === 'shard' && container.get(controller_decorator.CooldownStore) instanceof controller_decorator.MemoryCooldownStore) {
1831
+ warnPerShardCooldowns(options.controllers, logger);
1832
+ }
1833
+ providers.bindAppPresenter(container, target, discordClient);
1834
+ providers.registerClientTheme(discordClient, container);
1835
+ controller_decorator.registerClientTranslator(discordClient, options.i18n);
1836
+ };
1837
+ 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');
1839
+ });
1802
1840
  }
1803
1841
  }
1804
1842
  MeoCordFactory.logger = new controller_decorator.Logger();