meocord 4.2.1 → 4.2.3

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 (137) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
  3. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
  4. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
  5. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +435 -182
  9. package/dist/cjs/decorator/index.cjs +116 -121
  10. package/dist/cjs/testing/index.cjs +1923 -808
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/package.json.template +9 -9
  13. package/dist/esm/bin/app-template/src/main.ts.template +3 -2
  14. package/dist/esm/bin/generator.js +6 -4
  15. package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
  16. package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
  17. package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
  18. package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
  19. package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
  20. package/dist/esm/bin/helper/service-generator.helper.js +6 -4
  21. package/dist/esm/bin/meocord.js +106 -48
  22. package/dist/esm/build/native-addons.js +75 -19
  23. package/dist/esm/build/rsbuild-config.js +11 -3
  24. package/dist/esm/build/typed-externals.js +15 -2
  25. package/dist/esm/common/cooldown-store.js +38 -14
  26. package/dist/esm/common/deprecation.js +4 -1
  27. package/dist/esm/common/execution-context.js +3 -1
  28. package/dist/esm/common/explained-error.js +7 -3
  29. package/dist/esm/common/log-level.js +13 -9
  30. package/dist/esm/common/logger.js +4 -2
  31. package/dist/esm/common/meocord-text.js +11 -6
  32. package/dist/esm/common/redis-cooldown-store.js +3 -7
  33. package/dist/esm/common/response/flags.js +17 -1
  34. package/dist/esm/common/response/install-context.js +27 -16
  35. package/dist/esm/common/response/presenter.js +7 -3
  36. package/dist/esm/common/response/response-state.js +32 -10
  37. package/dist/esm/common/sharded-cooldown-store.js +11 -9
  38. package/dist/esm/common/theme.js +3 -1
  39. package/dist/esm/common/translator.js +27 -8
  40. package/dist/esm/core/autocomplete-routes.js +26 -0
  41. package/dist/esm/core/command-conflicts.js +15 -7
  42. package/dist/esm/core/command-registration.js +15 -1
  43. package/dist/esm/core/cooldown-runner.js +7 -9
  44. package/dist/esm/core/defer.js +2 -5
  45. package/dist/esm/core/dispatcher.js +92 -66
  46. package/dist/esm/core/event-requirements.js +67 -19
  47. package/dist/esm/core/event-sources.js +100 -0
  48. package/dist/esm/core/fallback.js +3 -1
  49. package/dist/esm/core/filter-runner.js +6 -6
  50. package/dist/esm/core/guard-runner.js +173 -52
  51. package/dist/esm/core/handler-input.js +24 -3
  52. package/dist/esm/core/handler-pipeline.js +54 -32
  53. package/dist/esm/core/handler-registry.js +51 -21
  54. package/dist/esm/core/input-runner.js +7 -8
  55. package/dist/esm/core/interceptor-runner.js +4 -5
  56. package/dist/esm/core/lifecycle-hooks.js +3 -8
  57. package/dist/esm/core/lifecycle-order.js +1 -15
  58. package/dist/esm/core/meocord-classes.js +14 -1
  59. package/dist/esm/core/meocord-factory.js +83 -54
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +37 -15
  62. package/dist/esm/core/message-params.js +126 -46
  63. package/dist/esm/core/message-routes.js +225 -45
  64. package/dist/esm/core/message-words.js +30 -2
  65. package/dist/esm/core/observer-runner.js +4 -2
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +39 -4
  68. package/dist/esm/core/scalar-types.js +57 -16
  69. package/dist/esm/core/shard-context.js +11 -10
  70. package/dist/esm/core/shard-exit.js +3 -3
  71. package/dist/esm/core/shard-manager.js +16 -6
  72. package/dist/esm/core/stage-scope.js +2 -1
  73. package/dist/esm/core/startup-roots.js +28 -8
  74. package/dist/esm/core/theme-resolvers.js +26 -11
  75. package/dist/esm/core/theme-runtime.js +23 -13
  76. package/dist/esm/core/theme-scope.js +31 -30
  77. package/dist/esm/core/theme-validation.js +1 -1
  78. package/dist/esm/decorator/app.decorator.js +37 -1
  79. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  80. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  81. package/dist/esm/decorator/controller.decorator.js +48 -19
  82. package/dist/esm/decorator/defer.decorator.js +2 -2
  83. package/dist/esm/decorator/event.decorator.js +4 -5
  84. package/dist/esm/decorator/filter.decorator.js +4 -4
  85. package/dist/esm/decorator/guard.decorator.js +4 -61
  86. package/dist/esm/decorator/inject.decorator.js +4 -1
  87. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  88. package/dist/esm/decorator/theme.decorator.js +4 -3
  89. package/dist/esm/decorator/validation.decorator.js +5 -5
  90. package/dist/esm/package.json.js +1 -1
  91. package/dist/esm/testing/catalog.js +32 -25
  92. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  93. package/dist/esm/testing/discord-defaults.js +449 -335
  94. package/dist/esm/testing/inspect-handler.js +91 -10
  95. package/dist/esm/testing/meocord-testing-module.js +217 -91
  96. package/dist/esm/testing/mock-fn.js +8 -2
  97. package/dist/esm/testing/mock-interaction.js +864 -124
  98. package/dist/esm/testing/response-log.js +8 -0
  99. package/dist/esm/testing/response.js +7 -5
  100. package/dist/esm/testing/routing.js +48 -6
  101. package/dist/esm/testing/strict-mocks.js +8 -4
  102. package/dist/esm/testing/theme.js +3 -2
  103. package/dist/esm/util/common.util.js +30 -9
  104. package/dist/esm/util/generator-cli.util.js +6 -4
  105. package/dist/esm/util/inherited-env.util.js +72 -13
  106. package/dist/esm/util/json.util.js +10 -3
  107. package/dist/esm/util/meocord-cli.util.js +3 -2
  108. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  109. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  110. package/dist/esm/util/meocord-source-config.util.js +13 -3
  111. package/dist/esm/util/platform.util.js +3 -2
  112. package/dist/esm/util/refusal.util.js +29 -11
  113. package/dist/esm/util/registration-mode.util.js +14 -1
  114. package/dist/esm/util/timer-limit.util.js +4 -1
  115. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  116. package/dist/esm/util/tsconfig.util.js +16 -58
  117. package/dist/esm/util/user-text.util.js +8 -4
  118. package/dist/esm/util/value.util.js +33 -5
  119. package/dist/types/common/index.d.cts +17 -10
  120. package/dist/types/common/index.d.ts +17 -10
  121. package/dist/types/core/index.d.cts +21 -11
  122. package/dist/types/core/index.d.ts +21 -11
  123. package/dist/types/decorator/index.d.cts +63 -63
  124. package/dist/types/decorator/index.d.ts +63 -63
  125. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  126. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  127. package/dist/types/interface/index.d.cts +1 -1
  128. package/dist/types/interface/index.d.ts +1 -1
  129. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  130. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  131. package/dist/types/testing/index.d.cts +158 -61
  132. package/dist/types/testing/index.d.ts +158 -61
  133. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  134. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  135. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  136. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  137. package/package.json +5 -2
@@ -1,6 +1,6 @@
1
1
  import 'reflect-metadata';
2
2
  import { assertStartupChecked } from './startup-checked.js';
3
- import { perHandler, injectedTokens, singletonContextError, sourcePrototype, stageClasses } from './guard-runner.js';
3
+ import { injectedTokens, singletonContextError, perHandler, sourcePrototype, stageClasses, methodStages } from './guard-runner.js';
4
4
  import { ExecutionContext } from '../common/execution-context.js';
5
5
  import { makeInjectable } from '../util/injectable.util.js';
6
6
  import { isAppClassToken } from './lifecycle-order.js';
@@ -24,13 +24,12 @@ function isInterceptorWithParams(entry) {
24
24
  }
25
25
  /**
26
26
  * The interceptors around a handler: each class's, base first, down to the controller it is dispatched on, then the
27
- * method's.
27
+ * method's and its overrides', base first.
28
28
  */ const handlerInterceptors = perHandler((prototype, methodName)=>{
29
- const source = sourcePrototype(prototype, methodName);
30
- if (!source) return [];
29
+ if (!sourcePrototype(prototype, methodName)) return [];
31
30
  return [
32
31
  ...stageClasses(prototype, methodName).flatMap((cls)=>Reflect.getOwnMetadata(META.classInterceptors, cls) ?? []),
33
- ...Reflect.getOwnMetadata(META.methodInterceptors, source, methodName) ?? []
32
+ ...methodStages(prototype, methodName, META.methodInterceptors)
34
33
  ];
35
34
  });
36
35
  /** Binds `cls` and its unbound dependencies as singletons, refusing any that injects `ExecutionContext`. */ function bindShared(container, cls) {
@@ -49,9 +49,9 @@ import { RunningCalls } from './running-calls.js';
49
49
  }
50
50
  /**
51
51
  * Resolves each unit and runs its `onReady` one at a time, in the units' dependency order. A failure is
52
- * reported and the next unit still runs. Each unit whose `onReady` settled, or that has none, is pushed
53
- * to `entries` as it finishes, so a shutdown that starts meanwhile closes exactly those.
54
- */ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled, runOnReady } = {}) {
52
+ * reported and the next unit still runs. `settled` hears of each unit as its `onReady` settles, or once it is found
53
+ * to have none, which is what a shutdown that starts meanwhile reads.
54
+ */ async function runReadyHooks(container, units, client, info, report, { stopped = ()=>false, slowAfterMs, settled, runOnReady } = {}) {
55
55
  const failed = new Set();
56
56
  // For each unit, the failed units it depends on, directly or through another dependency
57
57
  const failedUpstream = new Map();
@@ -88,11 +88,7 @@ import { RunningCalls } from './running-calls.js';
88
88
  continue;
89
89
  }
90
90
  firstToken.set(instance, unit.token);
91
- const entry = lifecycleEntry(unit, instance);
92
- // Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready waits for the one still
93
- // starting, within its time, and skips those not reached yet
94
91
  if (typeof instance.onReady !== 'function') {
95
- entries.push(entry);
96
92
  settled?.(unit);
97
93
  continue;
98
94
  }
@@ -109,7 +105,6 @@ import { RunningCalls } from './running-calls.js';
109
105
  } finally{
110
106
  clearTimeout(slow);
111
107
  }
112
- entries.push(entry);
113
108
  settled?.(unit);
114
109
  }
115
110
  }
@@ -1,5 +1,4 @@
1
1
  import { Client } from 'discord.js';
2
- import { injectedTokens } from './guard-runner.js';
3
2
 
4
3
  /**
5
4
  * The whole source the runtime gives a native function, such as `function NumberFormat() { [native code] }`. A class's
@@ -11,18 +10,5 @@ import { injectedTokens } from './guard-runner.js';
11
10
  */ function isAppClassToken(token) {
12
11
  return typeof token === 'function' && token !== Client && !NATIVE_SOURCE.test(Function.prototype.toString.call(token));
13
12
  }
14
- /**
15
- * The bound classes `cls` injects, by constructor type or `@inject` token: the classes whose hooks
16
- * run before its own.
17
- */ function lifecycleDependencies(container, cls) {
18
- return injectedTokens(cls).filter((token)=>isAppClassToken(token) && container.isBound(token));
19
- }
20
- /** The units for a list of classes already in dependency order, as the app runs them without providers. */ function classUnits(container, classes) {
21
- return classes.map((cls)=>({
22
- token: cls,
23
- name: cls.name,
24
- dependencies: lifecycleDependencies(container, cls)
25
- }));
26
- }
27
13
 
28
- export { classUnits, isAppClassToken, lifecycleDependencies };
14
+ export { isAppClassToken };
@@ -1,3 +1,4 @@
1
+ import { Client } from 'discord.js';
1
2
  import { CooldownStore } from '../common/cooldown-store.js';
2
3
  import { CooldownError, CooldownStoreError, GuardDeniedError, MessageUsageError, UserError, ValidationError } from '../common/errors.js';
3
4
  import { ExecutionContext } from '../common/execution-context.js';
@@ -16,6 +17,18 @@ import { ThemeCache } from './theme-resolvers.js';
16
17
  ThemeCache,
17
18
  Translator
18
19
  ];
20
+ /**
21
+ * The tokens the bot binds before an app's providers, which `@MeoCord({ providers })` cannot provide: `Translator` too
22
+ * when the app gives `i18n`.
23
+ */ const boundBeforeProviders = (i18n)=>[
24
+ Client,
25
+ HandlerRegistry,
26
+ ShardContext,
27
+ CooldownStore,
28
+ ...i18n ? [
29
+ Translator
30
+ ] : []
31
+ ];
19
32
  const created = (cls)=>`create it with \`new ${cls.name}(…)\``;
20
33
  /** What to do in place of injecting each of MeoCord's classes an app makes itself; the testing module adds its own. */ const advice = new Map([
21
34
  [
@@ -45,4 +58,4 @@ const created = (cls)=>`create it with \`new ${cls.name}(…)\``;
45
58
  advice.set(cls, instead);
46
59
  }
47
60
 
48
- export { adviseInPlaceOfInjecting, meocordClassAdvice, meocordClasses };
61
+ export { adviseInPlaceOfInjecting, boundBeforeProviders, meocordClassAdvice, meocordClasses };
@@ -3,9 +3,7 @@ import { COOLDOWN_POLICY, cooldownPolicyFrom, handlerCooldowns } from './cooldow
3
3
  import { Container } from 'inversify';
4
4
  import { Client } from 'discord.js';
5
5
  import { Logger } from '../common/logger.js';
6
- import { warnOnce } from '../common/deprecation.js';
7
- import './theme-scope.js';
8
- import './theme-validation.js';
6
+ import '../common/theme.js';
9
7
  import '../common/decorator.js';
10
8
  import { ExecutionContext } from '../common/execution-context.js';
11
9
  import '../common/errors.js';
@@ -13,31 +11,34 @@ import { CooldownStore, MemoryCooldownStore } from '../common/cooldown-store.js'
13
11
  import 'node:crypto';
14
12
  import { getCommandMap, getMessageHandlers } from '../decorator/controller.decorator.js';
15
13
  import './scalar-types.js';
14
+ import '../common/deprecation.js';
16
15
  import { processShardingEnabled, shardingRole, clientOptionsWithSharding } from '../util/sharding-mode.util.js';
17
- import { Translator, missingTranslatorError } from '../common/translator.js';
16
+ import { Translator } from '../common/translator.js';
18
17
  import { registerClientTranslator } from '../common/meocord-text.js';
19
18
  import '../common/response/response-state.js';
20
19
  import { isExplainedError, markExplained } from '../common/explained-error.js';
21
- import { themeResolverClass, ThemeCache } from './theme-resolvers.js';
20
+ import './theme-scope.js';
21
+ import { themeResolverClass } from './theme-resolvers.js';
22
22
  import { MeoCordApp } from './meocord.app.js';
23
23
  import { loadMeoCordConfig, compiledConfigMessage } from '../util/meocord-config-loader.util.js';
24
24
  import { bunDevelopmentValues, bunDevelopmentWarning } from '../util/inherited-env.util.js';
25
25
  import { isRegisterOnly } from '../util/registration-mode.util.js';
26
- import { recordInstanceContainers, injectedTokens, singletonContextError } from './guard-runner.js';
26
+ import { prepareDirectCalls, injectedTokens, singletonContextError } from './guard-runner.js';
27
27
  import { appStages, bindGlobalStages, prepareHandlerStages, prepareThemes, bindAppPresenter } from './handler-pipeline.js';
28
- import { startupClasses, assertStartupClasses, lateGuardCheck, stageDependencies } from './startup-roots.js';
28
+ import { startupClasses, assertStartupClasses, lateGuardCheck, stageDependencies, perCallEventWarnings } from './startup-roots.js';
29
+ import { eventSources } from './event-sources.js';
29
30
  import { markStartupChecked, assertStartupChecked } from './startup-checked.js';
30
31
  import { makeInjectable } from '../util/injectable.util.js';
31
32
  import { meocordClasses } from './meocord-classes.js';
32
33
  import { buildMessageRoutes } from './message-routes.js';
33
34
  import { appObservers, bindObservers } from './observer-runner.js';
34
35
  import { isAppClassToken } from './lifecycle-order.js';
35
- import { reachableClasses, providerMap, bindsOwnToken, tokenName, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies, resolveProviders } from './providers.js';
36
- import { HandlerRegistry } from './handler-registry.js';
36
+ import { providerMap, assertProvidedServices, bindsOwnToken, appProviderRefusal, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies, tokenName, resolveProviders, isValueProvider, forgetFactoryValues } from './providers.js';
37
+ import { HandlerRegistry, listEventSources } from './handler-registry.js';
37
38
  import { ShardManager } from './shard-manager.js';
38
39
  import { warnUnregisteredCommands, warnInheritedRoutes, warnOverlappingPatterns, warnHandlersOffControllers, assertDistinctCommands } from './command-conflicts.js';
39
40
  import { ShardContext, SHARD_CALL_KEY, shardCallHandler } from './shard-context.js';
40
- import { registerClientTheme } from './theme-runtime.js';
41
+ import { prepareLateThemes, registerClientTheme } from './theme-runtime.js';
41
42
  import { isRefusal, logStartupErrors, describeRefusal, allDeclaredErrors, declaredErrorsOf, collectStartupErrors, refuse, startupError, stopOnStartupErrors } from '../util/refusal.util.js';
42
43
  import { endFailedShard } from './shard-exit.js';
43
44
  import { isBuiltApplication } from '../util/bundle-entry.util.js';
@@ -55,8 +56,7 @@ import { isShardProcess } from '../util/shard-process.util.js';
55
56
  container.bind(cls).toSelf().inSingletonScope();
56
57
  // By constructor type or @inject token; an interface-typed parameter records Object, which is skipped
57
58
  for (const dep of injectedTokens(cls)){
58
- if (dep === Translator && !container.isBound(Translator)) startupError(missingTranslatorError(cls));
59
- else if (isAppClassToken(dep)) bindDependencies(container, dep, providers);
59
+ if (isAppClassToken(dep)) bindDependencies(container, dep, providers);
60
60
  }
61
61
  }
62
62
  /**
@@ -145,6 +145,10 @@ import { isShardProcess } from '../util/shard-process.util.js';
145
145
  markExplained(error);
146
146
  }
147
147
  }
148
+ // A built bot that refuses its app exits 1, as one whose start fails does, whatever main.ts does with the error
149
+ if (!shard && isBuiltApplication() && isRefusal(error) && (process.exitCode === undefined || process.exitCode === 0)) {
150
+ process.exitCode = 1;
151
+ }
148
152
  throw error;
149
153
  }
150
154
  }
@@ -176,28 +180,30 @@ import { isShardProcess } from '../util/shard-process.util.js';
176
180
  const providers = collectStartupErrors(()=>{
177
181
  declared.forEach(startupError);
178
182
  assertDistinctCommands(options.controllers);
179
- return providerMap(options.providers ?? [], '@MeoCord({ providers })');
183
+ const providers = providerMap(options.providers ?? [], '@MeoCord({ providers })');
184
+ assertProvidedServices(typeof target === 'function' ? target.name : String(target), options.services ?? [], providers, '@MeoCord({ providers })');
185
+ return providers;
180
186
  });
181
187
  // A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
182
188
  const themeResolver = themeResolverClass(options.themeFor);
183
- // The app's classes and every class they inject, which the container binds
184
- const roots = [
185
- ...options.controllers,
186
- ...options.services ?? [],
187
- ...themeResolver ? [
188
- themeResolver
189
- ] : [],
190
- ...options.cooldownStore ? [
191
- options.cooldownStore
192
- ] : [],
193
- ...appObservers(target)
194
- ];
189
+ // Every class the app runs, its stages and presenter and what they inject included
190
+ const stages = appStages(target);
191
+ const runs = startupClasses({
192
+ controllers: options.controllers,
193
+ services: options.services ?? [],
194
+ providers,
195
+ stages,
196
+ presenter: options.presenter,
197
+ cooldownStore: options.cooldownStore,
198
+ themeFor: options.themeFor,
199
+ observers: appObservers(target)
200
+ });
195
201
  // A shard's manager runs the same checks, so a sharded bot warns once
196
202
  if (!shard) {
197
203
  warnUnregisteredCommands(options.controllers);
198
204
  warnInheritedRoutes(options.controllers);
199
205
  warnOverlappingPatterns(options.controllers);
200
- warnHandlersOffControllers(options.controllers, reachableClasses(roots, providers));
206
+ warnHandlersOffControllers(options.controllers, runs.classes);
201
207
  const developmentEnv = bunDevelopmentValues();
202
208
  if (developmentEnv.keys.length > 0) {
203
209
  this.logger.warn(bunDevelopmentWarning(developmentEnv));
@@ -218,19 +224,8 @@ import { isShardProcess } from '../util/shard-process.util.js';
218
224
  }
219
225
  // The checks of what the app binds and runs report every error each finds, and the first is thrown
220
226
  return collectStartupErrors(()=>{
221
- // Every class the app runs, its stages and presenter included, checked before binding, where inversify would
222
- // otherwise fail first with an error about compiler options, or a guard only at its first call
223
- const stages = appStages(target);
224
- const runs = startupClasses({
225
- controllers: options.controllers,
226
- services: options.services ?? [],
227
- providers,
228
- stages,
229
- presenter: options.presenter,
230
- cooldownStore: options.cooldownStore,
231
- themeFor: options.themeFor,
232
- observers: appObservers(target)
233
- });
227
+ // Every class the app runs checked before binding, where inversify would otherwise fail first with an error about
228
+ // compiler options, or a guard only at its first call
234
229
  assertStartupClasses(runs, {
235
230
  translator: options.i18n !== undefined || providers.has(Translator)
236
231
  });
@@ -255,7 +250,8 @@ import { isShardProcess } from '../util/shard-process.util.js';
255
250
  }
256
251
  // Bound before the app's classes, so a class that injects it gets this instance; filled once they are bound
257
252
  const appClasses = [];
258
- container.bind(HandlerRegistry).toConstantValue(new HandlerRegistry(appClasses, options.messages, ()=>options.i18n));
253
+ const registry = new HandlerRegistry(appClasses, options.messages, ()=>options.i18n, options.controllers);
254
+ container.bind(HandlerRegistry).toConstantValue(registry);
259
255
  container.bind(ShardContext).toDynamicValue(()=>{
260
256
  const client = container.get(Client);
261
257
  return new ShardContext(client, (service, method, args)=>Reflect.get(client, SHARD_CALL_KEY)(service, method, args));
@@ -275,12 +271,9 @@ import { isShardProcess } from '../util/shard-process.util.js';
275
271
  // After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
276
272
  // a class token that is provided is not also bound as itself
277
273
  for (const [token, provider] of providers){
278
- // Bound later, or for each call, so not caught below; supported until 5.0 refuses it
279
- if (token === ThemeCache || token === ExecutionContext) {
280
- warnOnce(this.logger, `${target.name}: @MeoCord({ providers }) provides ${tokenName(token)}, which MeoCord binds itself; ` + 'the next major version (5.0) refuses it. Remove the provider.');
281
- }
282
- if (container.isBound(token)) {
283
- startupError(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${tokenName(token)}, which MeoCord binds itself.`));
274
+ const refusal = appProviderRefusal(`${target.name}`, token, (own)=>container.isBound(own), this.logger);
275
+ if (refusal) {
276
+ startupError(refusal);
284
277
  continue;
285
278
  }
286
279
  bindProvider(container, provider, (cls)=>bindDependencies(container, cls, providers));
@@ -365,26 +358,35 @@ import { isShardProcess } from '../util/shard-process.util.js';
365
358
  buildMessageRoutes(options.controllers, options.messages);
366
359
  // Every check has run, and touched only the container: from here the app is made, once none found an error
367
360
  stopOnStartupErrors();
361
+ for (const warning of perCallEventWarnings(runs, providers, options.presenter))this.logger.warn(warning);
368
362
  const discordClient = container.get(Client);
369
363
  Reflect.set(discordClient, SHARD_CALL_KEY, shardCallHandler(container, ()=>callable, 'this app'));
370
- // Stamp every class the app runs with the container, so @UseGuard resolves guards on a direct call to any of them
371
- for (const cls of runs.classes){
372
- Reflect.defineMetadata(META.container, container, cls);
373
- }
374
- recordInstanceContainers(container, runs.classes);
364
+ // Every class the app runs, so @UseGuard resolves guards on a direct call to any of them
365
+ prepareDirectCalls(container, runs.classes);
375
366
  prepareThemes(container, appClasses);
376
367
  // Run by start() before it logs in: what may inject a provided value is resolved once every factory
377
368
  // has made its value, including those that return a promise
378
369
  const logger = this.logger;
370
+ let sources;
379
371
  const startup = async (control)=>{
372
+ let made;
380
373
  try {
381
- await resolveProviders(container, providers, order, control);
374
+ made = await resolveProviders(container, providers, order, control);
382
375
  } catch (error) {
383
376
  logger.error(`${error.message}. The bot cannot start without it.`);
384
377
  logger.debug('Provider failure:', error.cause);
385
378
  markExplained(error);
386
379
  throw error;
387
380
  }
381
+ // A stop came while the providers were being made: what follows resolves provided values, which would make the
382
+ // factories it left unmade
383
+ if (!made) return;
384
+ // Read once every provided value is made, and before anything the app makes could list its handlers
385
+ if (!sources) {
386
+ sources = eventSources(container, providers, lifecycle, appClasses, (text)=>logger.warn(text));
387
+ listEventSources(registry, sources);
388
+ prepareLateThemes(container, sources.map(({ cls })=>cls));
389
+ }
388
390
  // The listed services are made now, so constructors that attach listeners or connect run before login; a stop
389
391
  // meanwhile makes no more of them
390
392
  for (const svc of options.services ?? []){
@@ -399,11 +401,38 @@ import { isShardProcess } from '../util/shard-process.util.js';
399
401
  registerClientTheme(discordClient, container);
400
402
  registerClientTranslator(discordClient, options.i18n);
401
403
  };
402
- 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
403
- options.warnUnanswered ?? process.env.NODE_ENV === 'development', shard);
404
+ return new MeoCordApp(options.controllers, container, discordClient, meocordConfig.discordToken, options.activities, ()=>sources ?? [], meocordConfig.shutdownTimeout, startup, lifecycle, options.messages, // In development only, unless the app says otherwise
405
+ options.warnUnanswered ?? process.env.NODE_ENV === 'development', shard, startAttempt(container, lifecycle, providers));
404
406
  });
405
407
  }
406
408
  }
407
409
  MeoCordFactory.logger = new Logger();
410
+ /**
411
+ * What the app's starts make: each lifecycle unit is recorded as the container constructs it, as the testing module
412
+ * records them, and a container snapshot marks where a start begins, so a failed start drops every singleton it made,
413
+ * the values factory providers keep included.
414
+ */ function startAttempt(container, lifecycle, providers) {
415
+ const constructed = new Set();
416
+ for (const { token } of lifecycle){
417
+ container.onActivation(token, (_context, instance)=>{
418
+ constructed.add(token);
419
+ return instance;
420
+ });
421
+ }
422
+ const drop = ()=>{
423
+ container.restore();
424
+ forgetFactoryValues(container);
425
+ };
426
+ const given = new Set(lifecycle.map(({ token })=>token).filter((token)=>{
427
+ const provider = providers.get(token);
428
+ return provider !== undefined && isValueProvider(provider);
429
+ }));
430
+ return {
431
+ constructed,
432
+ given,
433
+ begin: ()=>container.snapshot(),
434
+ drop
435
+ };
436
+ }
408
437
 
409
438
  export { MeoCordFactory };