meocord 4.2.0 → 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 (95) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-XKMlcUZz.cjs} +376 -100
  4. package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-BZgJyfZS.cjs} +145 -36
  5. package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-CgfPQrNl.cjs} +219 -63
  7. package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-BUAR1WYG.cjs} +10 -5
  8. package/dist/cjs/common/index.cjs +3 -3
  9. package/dist/cjs/core/index.cjs +185 -168
  10. package/dist/cjs/decorator/index.cjs +21 -6
  11. package/dist/cjs/testing/index.cjs +1594 -286
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/generator.js +4 -5
  14. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  15. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  16. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  17. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  18. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  19. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  20. package/dist/esm/bin/meocord.js +84 -25
  21. package/dist/esm/build/load-config.pre-entry.js +13 -3
  22. package/dist/esm/build/rsbuild-config.js +16 -12
  23. package/dist/esm/build/typed-externals.js +36 -0
  24. package/dist/esm/common/cooldown-store.js +10 -2
  25. package/dist/esm/common/errors.js +25 -0
  26. package/dist/esm/common/meocord-text.js +11 -8
  27. package/dist/esm/common/response/call-order.js +9 -4
  28. package/dist/esm/common/response/presenter.js +33 -6
  29. package/dist/esm/common/response/response-state.js +84 -10
  30. package/dist/esm/common/route.js +11 -4
  31. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  32. package/dist/esm/common/translator.js +37 -6
  33. package/dist/esm/core/command-registration.js +93 -24
  34. package/dist/esm/core/command-routes.js +2 -2
  35. package/dist/esm/core/cooldown-runner.js +29 -8
  36. package/dist/esm/core/fallback.js +20 -11
  37. package/dist/esm/core/guard-runner.js +16 -3
  38. package/dist/esm/core/handler-input.js +4 -3
  39. package/dist/esm/core/lifecycle-hooks.js +134 -47
  40. package/dist/esm/core/meocord-factory.js +55 -32
  41. package/dist/esm/core/meocord.app.js +116 -75
  42. package/dist/esm/core/providers.js +7 -2
  43. package/dist/esm/core/running-calls.js +43 -0
  44. package/dist/esm/core/shard-context.js +5 -6
  45. package/dist/esm/core/shard-manager.js +16 -10
  46. package/dist/esm/core/startup-roots.js +21 -1
  47. package/dist/esm/core/theme-resolvers.js +4 -1
  48. package/dist/esm/core/theme-scope.js +3 -1
  49. package/dist/esm/core/theme-validation.js +17 -10
  50. package/dist/esm/decorator/controller.decorator.js +56 -20
  51. package/dist/esm/decorator/cooldown.decorator.js +17 -0
  52. package/dist/esm/decorator/guard.decorator.js +3 -2
  53. package/dist/esm/package.json.js +1 -1
  54. package/dist/esm/testing/catalog.js +26 -10
  55. package/dist/esm/testing/cooldown-store-suite.js +67 -5
  56. package/dist/esm/testing/discord-defaults.js +205 -30
  57. package/dist/esm/testing/meocord-testing-module.js +134 -28
  58. package/dist/esm/testing/mock-fn.js +107 -26
  59. package/dist/esm/testing/mock-interaction.js +1026 -172
  60. package/dist/esm/testing/snowflake.js +9 -2
  61. package/dist/esm/testing/strict-mocks.js +37 -17
  62. package/dist/esm/util/common.util.js +47 -19
  63. package/dist/esm/util/dev-runner.util.js +8 -8
  64. package/dist/esm/util/generator-cli.util.js +4 -5
  65. package/dist/esm/util/interaction.util.js +2 -2
  66. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  67. package/dist/esm/util/meocord-config-validation.util.js +3 -5
  68. package/dist/esm/util/meocord-source-config.util.js +65 -20
  69. package/dist/esm/util/method-wrapper.util.js +16 -0
  70. package/dist/esm/util/platform.util.js +10 -10
  71. package/dist/esm/util/shard-process.util.js +4 -1
  72. package/dist/esm/util/sharding-mode.util.js +5 -7
  73. package/dist/esm/util/shared-state.util.js +21 -0
  74. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  75. package/dist/esm/util/tsconfig.util.js +10 -10
  76. package/dist/esm/util/value.util.js +12 -1
  77. package/dist/types/common/index.d.cts +6 -6
  78. package/dist/types/common/index.d.ts +6 -6
  79. package/dist/types/core/index.d.cts +3 -2
  80. package/dist/types/core/index.d.ts +3 -2
  81. package/dist/types/decorator/index.d.cts +1 -0
  82. package/dist/types/decorator/index.d.ts +1 -0
  83. package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
  84. package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
  85. package/dist/types/interface/index.d.cts +1 -1
  86. package/dist/types/interface/index.d.ts +1 -1
  87. package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
  88. package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
  89. package/dist/types/testing/index.d.cts +175 -53
  90. package/dist/types/testing/index.d.ts +175 -53
  91. package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  92. package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  93. package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
  94. package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
  95. package/package.json +2 -2
@@ -1,4 +1,6 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  import { storeOperationsSettled } from './cooldown-runner.js';
3
+ import { RunningCalls } from './running-calls.js';
2
4
 
3
5
  /** The entry of a unit whose instance is `instance`. */ function lifecycleEntry(unit, instance) {
4
6
  return {
@@ -11,11 +13,45 @@ import { storeOperationsSettled } from './cooldown-runner.js';
11
13
  }
12
14
  };
13
15
  }
16
+ /**
17
+ * The ready hooks' pass, which shutdown waits for before it reads which units to shut down, so a unit whose `onReady`
18
+ * finishes meanwhile is shut down too. An `onReady` still running that asks the app to stop is not waited for; a timer
19
+ * or callback an `onReady` that has settled left behind excuses nothing.
20
+ */ class ReadyPass {
21
+ /** Runs `pass`, the ready hooks, as the pass shutdown waits for. */ run(pass) {
22
+ return this.passes.run('onReady', pass);
23
+ }
24
+ /** Excuses the pass when this runs within the `onReady` in progress, as one that stops the app does. */ excuseCurrent() {
25
+ if (this.current && this.hooks.getStore() === this.current) this.passes.excuseCurrent();
26
+ }
27
+ /** Settles once the pass has, unless excused. */ settled() {
28
+ return this.passes.settled();
29
+ }
30
+ /** The unit whose `onReady` a wait would still wait for, if any. */ running() {
31
+ return this.passes.running().length > 0 ? [
32
+ this.unit ?? 'a unit'
33
+ ] : [];
34
+ }
35
+ constructor(){
36
+ this.passes = new RunningCalls();
37
+ /** The `onReady` each async context belongs to. */ this.hooks = new AsyncLocalStorage();
38
+ /** Runs a unit's `onReady` as the one in progress. */ this.runOnReady = async (unit, run)=>{
39
+ const hook = {};
40
+ this.current = hook;
41
+ this.unit = unit.name;
42
+ try {
43
+ await this.hooks.run(hook, run);
44
+ } finally{
45
+ if (this.current === hook) this.current = undefined;
46
+ }
47
+ };
48
+ }
49
+ }
14
50
  /**
15
51
  * Resolves each unit and runs its `onReady` one at a time, in the units' dependency order. A failure is
16
52
  * reported and the next unit still runs. Each unit whose `onReady` settled, or that has none, is pushed
17
53
  * to `entries` as it finishes, so a shutdown that starts meanwhile closes exactly those.
18
- */ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled } = {}) {
54
+ */ async function runReadyHooks(container, units, client, info, entries, report, { stopped = ()=>false, slowAfterMs, settled, runOnReady } = {}) {
19
55
  const failed = new Set();
20
56
  // For each unit, the failed units it depends on, directly or through another dependency
21
57
  const failedUpstream = new Map();
@@ -23,6 +59,9 @@ import { storeOperationsSettled } from './cooldown-runner.js';
23
59
  unit.token,
24
60
  unit
25
61
  ]));
62
+ // The token each instance was first reached by: one instance two tokens reach, an alias or one value provided twice,
63
+ // runs its hooks once
64
+ const firstToken = new Map();
26
65
  for (const unit of units){
27
66
  if (stopped()) break;
28
67
  const upstream = new Set();
@@ -41,9 +80,17 @@ import { storeOperationsSettled } from './cooldown-runner.js';
41
80
  settled?.(unit);
42
81
  continue;
43
82
  }
83
+ const first = firstToken.get(instance);
84
+ if (first !== undefined) {
85
+ // Failed or not as the token that ran its hooks did, for what depends on this one
86
+ if (failed.has(first)) failed.add(unit.token);
87
+ settled?.(unit);
88
+ continue;
89
+ }
90
+ firstToken.set(instance, unit.token);
44
91
  const entry = lifecycleEntry(unit, instance);
45
- // Only a unit whose onReady has settled, or that has none, is shut down: a stop mid-ready skips
46
- // the one still starting, and those not reached yet
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
47
94
  if (typeof instance.onReady !== 'function') {
48
95
  entries.push(entry);
49
96
  settled?.(unit);
@@ -54,7 +101,8 @@ import { storeOperationsSettled } from './cooldown-runner.js';
54
101
  ]);
55
102
  const slow = slowAfterMs !== undefined && report.slow ? setTimeout(()=>report.slow(unit), slowAfterMs) : undefined;
56
103
  try {
57
- await instance.onReady(client, info);
104
+ const onReady = ()=>instance.onReady(client, info);
105
+ await (runOnReady ? runOnReady(unit, onReady) : onReady());
58
106
  } catch (error) {
59
107
  failed.add(unit.token);
60
108
  report.hookFailed(unit, error);
@@ -67,12 +115,14 @@ import { storeOperationsSettled } from './cooldown-runner.js';
67
115
  }
68
116
  /**
69
117
  * Runs the entries' `onShutdown` hooks one at a time, in reverse, so a unit stops before those it
70
- * depends on. A failure is reported and the next hook still runs.
71
- */ async function runShutdownHooks(entries, hookFailed) {
118
+ * depends on. A failure is reported and the next hook still runs. An instance already in `ran` is not shut down again,
119
+ * so one that two entries reach stops once.
120
+ */ async function runShutdownHooks(entries, hookFailed, ran = new Set()) {
72
121
  for (const { name, instance } of [
73
122
  ...entries
74
123
  ].reverse()){
75
- if (typeof instance.onShutdown !== 'function') continue;
124
+ if (typeof instance.onShutdown !== 'function' || ran.has(instance)) continue;
125
+ ran.add(instance);
76
126
  try {
77
127
  await instance.onShutdown();
78
128
  } catch (error) {
@@ -80,54 +130,91 @@ import { storeOperationsSettled } from './cooldown-runner.js';
80
130
  }
81
131
  }
82
132
  }
83
- /**
84
- * Shuts down the entries there are when it begins, as a bot and a testing module both do. When the cooldown store, or
85
- * anything it injects directly or through another, has an `onShutdown`, the calls under way finish first, then the
86
- * store operations they started, so the store's last writes and releases still reach it. The other hooks then run in
87
- * reverse dependency order, and the store and what it injects last. The whole sequence is waited for at most
88
- * `timeoutMs`.
89
- */ async function runShutdownSequence(container, entries, { drainCalls, timeoutMs, hookFailed, warn }) {
90
- // A unit whose onReady settles while the calls drain was still starting when shutdown began, so it is not shut down
91
- entries = [
92
- ...entries
93
- ];
94
- const byToken = new Map(entries.map((entry)=>[
95
- entry.token,
96
- entry
97
- ]));
98
- // The store and everything it reaches through what it injects
99
- const storeSide = new Set();
100
- const reach = (entry)=>{
101
- if (!entry || storeSide.has(entry)) return;
102
- storeSide.add(entry);
103
- for (const dependency of entry.dependencies)reach(byToken.get(dependency));
104
- };
105
- for (const entry of entries)if (entry.cooldownStore) reach(entry);
106
- const needsStore = [
107
- ...storeSide
108
- ].some((entry)=>typeof entry.instance.onShutdown === 'function');
109
- const sequence = (async ()=>{
110
- if (needsStore) {
111
- await drainCalls();
112
- await storeOperationsSettled(container);
113
- }
114
- // Nothing on the store's side injects anything outside it, so running it last keeps the reverse dependency order
115
- await runShutdownHooks(entries.filter((entry)=>!storeSide.has(entry)), hookFailed);
116
- await runShutdownHooks(entries.filter((entry)=>storeSide.has(entry)), hookFailed);
117
- })();
133
+ /** The part of `timeoutMs` the drain leaves for the hooks: a quarter, at least 1 s, and never more than half. */ function hooksReserveMs(timeoutMs) {
134
+ return Math.min(Math.max(timeoutMs / 4, 1000), timeoutMs / 2);
135
+ }
136
+ /** A timer whose promise resolves with `value` after `ms`, and `cancel` to clear it. */ function after(ms, value) {
118
137
  let timer;
119
- const timedOut = new Promise((resolve)=>{
120
- timer = setTimeout(()=>resolve('timeout'), timeoutMs);
138
+ const promise = new Promise((resolve)=>{
139
+ timer = setTimeout(()=>resolve(value), ms);
121
140
  });
141
+ return {
142
+ promise,
143
+ cancel: ()=>clearTimeout(timer)
144
+ };
145
+ }
146
+ /**
147
+ * Shuts down the entries there are once an `onReady` in progress has settled, as a bot and a testing module both do.
148
+ * The calls under way finish first, on every shutdown with `drainAlways`, and otherwise when the cooldown store, or
149
+ * anything it injects directly or through another, has an `onShutdown`; then the store operations they started, so the
150
+ * store's last writes and releases still reach it. The other hooks then run in reverse dependency order, and the store
151
+ * and what it injects last.
152
+ *
153
+ * The whole sequence is waited for at most `timeoutMs`. The wait for the `onReady` and the drain stop early enough to
154
+ * leave the hooks {@link hooksReserveMs}, with a warning naming what still runs, and the hooks run in the time left.
155
+ */ async function runShutdownSequence(container, readEntries, { drainCalls, drainAlways = false, runningCalls, readyPass, timeoutMs, hookFailed, warn }) {
156
+ const whole = after(timeoutMs, 'timeout');
157
+ const reserve = hooksReserveMs(timeoutMs);
158
+ const drainEnds = after(timeoutMs - reserve, false);
159
+ // At once on a shutdown that always drains, so no call starts while an onReady finishes
160
+ let draining = drainAlways ? drainCalls().then(()=>true) : undefined;
122
161
  try {
162
+ const readying = readyPass?.running() ?? [];
163
+ if (readying.length > 0 && !await Promise.race([
164
+ readyPass.settled().then(()=>true),
165
+ drainEnds.promise
166
+ ])) {
167
+ warn(`onReady in ${readying.join(', ')} is still running after ${timeoutMs - reserve} ms; shutting down without waiting for it.`);
168
+ }
169
+ // Read once the onReady in progress has settled, so its unit is among them
170
+ const entries = [
171
+ ...readEntries()
172
+ ];
173
+ const byToken = new Map(entries.map((entry)=>[
174
+ entry.token,
175
+ entry
176
+ ]));
177
+ // The store and everything it reaches through what it injects
178
+ const storeSide = new Set();
179
+ const reach = (entry)=>{
180
+ if (!entry || storeSide.has(entry)) return;
181
+ storeSide.add(entry);
182
+ for (const dependency of entry.dependencies)reach(byToken.get(dependency));
183
+ };
184
+ for (const entry of entries)if (entry.cooldownStore) reach(entry);
185
+ const needsStore = [
186
+ ...storeSide
187
+ ].some((entry)=>typeof entry.instance.onShutdown === 'function');
188
+ let drained = true;
189
+ if (needsStore) draining ?? (draining = drainCalls().then(()=>true));
190
+ if (draining) {
191
+ drained = await Promise.race([
192
+ draining,
193
+ drainEnds.promise
194
+ ]);
195
+ if (!drained) {
196
+ const running = runningCalls?.() ?? [];
197
+ warn(`Calls still running after ${timeoutMs - reserve} ms${running.length > 0 ? `: ${running.join(', ')}` : ''}; ` + `running the onShutdown hooks in the ${reserve} ms left.`);
198
+ }
199
+ }
200
+ drainEnds.cancel();
201
+ const sequence = (async ()=>{
202
+ // Only after the calls settled: a call still running may never let its store operations settle
203
+ if (needsStore && drained) await storeOperationsSettled(container);
204
+ // Nothing on the store's side injects anything outside it, so running it last keeps the reverse dependency order
205
+ const ran = new Set();
206
+ await runShutdownHooks(entries.filter((entry)=>!storeSide.has(entry)), hookFailed, ran);
207
+ await runShutdownHooks(entries.filter((entry)=>storeSide.has(entry)), hookFailed, ran);
208
+ })();
123
209
  if (await Promise.race([
124
210
  sequence,
125
- timedOut
211
+ whole.promise
126
212
  ]) === 'timeout') {
127
213
  warn(`onShutdown hooks did not finish within ${timeoutMs} ms; shutting down anyway.`);
128
214
  }
129
215
  } finally{
130
- clearTimeout(timer);
216
+ drainEnds.cancel();
217
+ whole.cancel();
131
218
  }
132
219
  }
133
220
  /** Settles once every promise in `calls` has, those added while waiting included. */ async function callsSettled(calls) {
@@ -136,4 +223,4 @@ import { storeOperationsSettled } from './cooldown-runner.js';
136
223
  ]);
137
224
  }
138
225
 
139
- export { callsSettled, lifecycleEntry, runReadyHooks, runShutdownHooks, runShutdownSequence };
226
+ export { ReadyPass, callsSettled, hooksReserveMs, lifecycleEntry, runReadyHooks, runShutdownHooks, runShutdownSequence };
@@ -3,32 +3,29 @@ 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';
6
7
  import './theme-scope.js';
7
8
  import './theme-validation.js';
8
9
  import '../common/decorator.js';
9
10
  import { ExecutionContext } from '../common/execution-context.js';
10
- import { registerClientTranslator } from '../common/meocord-text.js';
11
+ import '../common/errors.js';
11
12
  import { CooldownStore, MemoryCooldownStore } from '../common/cooldown-store.js';
12
13
  import 'node:crypto';
13
14
  import { getCommandMap, getMessageHandlers } from '../decorator/controller.decorator.js';
14
15
  import './scalar-types.js';
15
16
  import { processShardingEnabled, shardingRole, clientOptionsWithSharding } from '../util/sharding-mode.util.js';
16
17
  import { Translator, missingTranslatorError } from '../common/translator.js';
17
- import '../common/response/send-failure.js';
18
- import '../util/interaction.util.js';
19
- import '../common/response/flags.js';
20
- import 'path';
21
- import '../common/response/components.js';
22
- import { themeResolverClass } from './theme-resolvers.js';
18
+ import { registerClientTranslator } from '../common/meocord-text.js';
19
+ import '../common/response/response-state.js';
23
20
  import { isExplainedError, markExplained } from '../common/explained-error.js';
21
+ import { themeResolverClass, ThemeCache } from './theme-resolvers.js';
24
22
  import { MeoCordApp } from './meocord.app.js';
25
23
  import { loadMeoCordConfig, compiledConfigMessage } from '../util/meocord-config-loader.util.js';
26
24
  import { bunDevelopmentValues, bunDevelopmentWarning } from '../util/inherited-env.util.js';
27
- import { assertBuiltForThisPlatform } from '../util/platform.util.js';
28
25
  import { isRegisterOnly } from '../util/registration-mode.util.js';
29
- import { injectedTokens, singletonContextError } from './guard-runner.js';
26
+ import { recordInstanceContainers, injectedTokens, singletonContextError } from './guard-runner.js';
30
27
  import { appStages, bindGlobalStages, prepareHandlerStages, prepareThemes, bindAppPresenter } from './handler-pipeline.js';
31
- import { startupClasses, assertStartupClasses, lateGuardCheck } from './startup-roots.js';
28
+ import { startupClasses, assertStartupClasses, lateGuardCheck, stageDependencies } from './startup-roots.js';
32
29
  import { markStartupChecked, assertStartupChecked } from './startup-checked.js';
33
30
  import { makeInjectable } from '../util/injectable.util.js';
34
31
  import { meocordClasses } from './meocord-classes.js';
@@ -102,8 +99,8 @@ import { isShardProcess } from '../util/shard-process.util.js';
102
99
  * The config a single process runs with. Under `meocord start --dev` without `sharding.development`,
103
100
  * process sharding falls back to running every shard in this process, so the watcher restarts one
104
101
  * process and leaves no shards behind.
105
- */ static effectiveConfig(config) {
106
- if (config.sharding?.mode !== 'process' || isShardProcess() || processShardingEnabled(config)) return config;
102
+ */ static effectiveConfig(config, shard) {
103
+ if (config.sharding?.mode !== 'process' || shard || processShardingEnabled(config)) return config;
107
104
  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.');
108
105
  return {
109
106
  ...config,
@@ -123,7 +120,8 @@ import { isShardProcess } from '../util/shard-process.util.js';
123
120
  * @returns The application, which `start()` logs in.
124
121
  * @throws Error when the class has no `@MeoCord`, when the compiled config is missing beside the bundle or fails to
125
122
  * load, naming the file and the reason, with what to do about it, when a provider cannot
126
- * be bound, such as one for a token MeoCord binds itself, when two handlers take one command, or two builder
123
+ * be bound, such as one for a token MeoCord binds itself (a provider for `ThemeCache` or `ExecutionContext` is warned
124
+ * about instead, and refused from the next major version, 5.0), when two handlers take one command, or two builder
127
125
  * classes build one, naming both, and for any other mistake it refuses as the app loads, such as two component
128
126
  * patterns that match the same customIds. Each check reports every error it finds, and the first is thrown,
129
127
  * unchanged. When there are several, each is logged first, with where in the source it comes from; a lone one is
@@ -131,11 +129,14 @@ import { isShardProcess } from '../util/shard-process.util.js';
131
129
  * shard of process sharding, the manager logs it. With `startupErrors: 'all'` in `meocord.config.ts`, the errors
132
130
  * decorators kept are among them.
133
131
  */ static create(target) {
132
+ // Decided first, once, and handed to all that asks: a ShardingManager this app builds writes the variable into its
133
+ // own process too, which would make the manager read as a shard
134
+ const shard = isShardProcess(process.env);
134
135
  try {
135
- return this.createApplication(target);
136
+ return this.createApplication(target, shard);
136
137
  } catch (error) {
137
138
  // A shard ends, so its manager restarts it, or stops every shard for a refusal, which it logs
138
- if (isShardProcess()) endFailedShard(error);
139
+ if (shard) endFailedShard(error);
139
140
  else if (isRefusal(error) && !isExplainedError(error)) {
140
141
  // Every startup error found together is logged, in a test too; a lone one is logged in a built application
141
142
  if (logStartupErrors(error, (text)=>this.logger.error(text), 'the bot did not start')) markExplained(error);
@@ -147,18 +148,25 @@ import { isShardProcess } from '../util/shard-process.util.js';
147
148
  throw error;
148
149
  }
149
150
  }
150
- static createApplication(target) {
151
- const options = Reflect.getMetadata(META.appOptions, target);
151
+ static createApplication(target, shard) {
152
+ const appOptions = Reflect.getMetadata(META.appOptions, target);
152
153
  // With startupErrors: 'all', what the decorators kept: a built bot's every one, else the app's and its controllers'
153
154
  const declared = isBuiltApplication() ? allDeclaredErrors() : declaredErrorsOf([
154
155
  target,
155
- ...options?.controllers ?? []
156
+ ...appOptions?.controllers ?? []
156
157
  ]);
157
- if (!options) {
158
+ if (!appOptions) {
158
159
  // A refused @MeoCord stores no options, so its own errors say why
159
160
  collectStartupErrors(()=>declared.forEach(startupError));
160
161
  throw refuse(new Error(`${typeof target === 'function' ? target.name : String(target)}: not decorated with @MeoCord(), so there is no app to create.`));
161
162
  }
163
+ // A controller listed twice is one controller, as the command checks and fromApp already take it
164
+ const options = {
165
+ ...appOptions,
166
+ controllers: [
167
+ ...new Set(appOptions.controllers)
168
+ ]
169
+ };
162
170
  const meocordConfig = loadMeoCordConfig();
163
171
  if (!meocordConfig) {
164
172
  throw refuse(new Error(compiledConfigMessage()));
@@ -185,7 +193,7 @@ import { isShardProcess } from '../util/shard-process.util.js';
185
193
  ...appObservers(target)
186
194
  ];
187
195
  // A shard's manager runs the same checks, so a sharded bot warns once
188
- if (!isShardProcess()) {
196
+ if (!shard) {
189
197
  warnUnregisteredCommands(options.controllers);
190
198
  warnInheritedRoutes(options.controllers);
191
199
  warnOverlappingPatterns(options.controllers);
@@ -200,11 +208,8 @@ import { isShardProcess } from '../util/shard-process.util.js';
200
208
  if (isRegisterOnly()) {
201
209
  return new MeoCordApp(options.controllers, new Container(), new Client(options.clientOptions), meocordConfig.discordToken);
202
210
  }
203
- // Before anything is resolved: a controller or service is what first loads a native addon, and one built for another
204
- // platform would otherwise fail there with a linker error. A manager checks too, before it registers or spawns.
205
- assertBuiltForThisPlatform();
206
211
  // A process-sharding manager only spawns shards, so it binds, constructs and connects nothing itself.
207
- if (shardingRole(meocordConfig) === 'manager') {
212
+ if (shardingRole(meocordConfig, shard) === 'manager') {
208
213
  return new ShardManager({
209
214
  controllerClasses: options.controllers,
210
215
  token: meocordConfig.discordToken,
@@ -242,7 +247,7 @@ import { isShardProcess } from '../util/shard-process.util.js';
242
247
  ], lateGuardCheck(providers, translator));
243
248
  bindGlobalStages(container, stages);
244
249
  // Bound now, so no provider stands in for it; made once the checks pass, the one Client the app logs in with
245
- const clientOptions = clientOptionsWithSharding(this.effectiveConfig(meocordConfig), options.clientOptions);
250
+ const clientOptions = clientOptionsWithSharding(this.effectiveConfig(meocordConfig, shard), options.clientOptions);
246
251
  container.bind(Client).toDynamicValue(()=>new Client(clientOptions)).inSingletonScope();
247
252
  if (options.i18n) {
248
253
  container.bind(Translator).toConstantValue(options.i18n);
@@ -270,6 +275,10 @@ import { isShardProcess } from '../util/shard-process.util.js';
270
275
  // After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
271
276
  // a class token that is provided is not also bound as itself
272
277
  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
+ }
273
282
  if (container.isBound(token)) {
274
283
  startupError(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${tokenName(token)}, which MeoCord binds itself.`));
275
284
  continue;
@@ -287,6 +296,9 @@ import { isShardProcess } from '../util/shard-process.util.js';
287
296
  // Observers are services too: bound here so their lifecycle hooks run in dependency order
288
297
  const observers = appObservers(target);
289
298
  for (const observer of observers)bindDependencies(container, observer, providers);
299
+ // What the stages and presenter inject, bound now so a service among them is made as the bot comes online
300
+ const stageServices = stageDependencies(runs, providers, options.presenter);
301
+ for (const service of stageServices)bindDependencies(container, service, providers);
290
302
  // What follows walks what these bound, so a class refused here is not reported again as missing
291
303
  stopOnStartupErrors();
292
304
  // Providers first, then the services, the controllers and the observers, each after what it depends on. The app's
@@ -307,12 +319,19 @@ import { isShardProcess } from '../util/shard-process.util.js';
307
319
  return isAppClassToken(token) && (!provider || isClassProvider(provider) && provider.useClass === token);
308
320
  }));
309
321
  assertProvided(container, providers, runs.classes, '@MeoCord({ providers })');
322
+ // With what the stages inject among them, each after what it depends on, so their hooks run as a listed service's do
323
+ const roots = stageServices.length > 0 ? resolutionOrder(container, providers, [
324
+ ...order,
325
+ ...stageServices
326
+ ], {
327
+ followOwnTokens: false
328
+ }) : order;
310
329
  // The store first, after only what it injects: it is ready before anything a call reaches, and shuts down last
311
330
  const store = options.cooldownStore;
312
331
  const lifecycle = (store ? resolutionOrder(container, providers, [
313
332
  store,
314
- ...order
315
- ]) : order).map((token)=>({
333
+ ...roots
334
+ ]) : roots).map((token)=>({
316
335
  token,
317
336
  name: tokenName(token),
318
337
  dependencies: tokenDependencies(container, providers, token),
@@ -352,24 +371,28 @@ import { isShardProcess } from '../util/shard-process.util.js';
352
371
  for (const cls of runs.classes){
353
372
  Reflect.defineMetadata(META.container, container, cls);
354
373
  }
374
+ recordInstanceContainers(container, runs.classes);
355
375
  prepareThemes(container, appClasses);
356
376
  // Run by start() before it logs in: what may inject a provided value is resolved once every factory
357
377
  // has made its value, including those that return a promise
358
378
  const logger = this.logger;
359
- const startup = async ()=>{
379
+ const startup = async (control)=>{
360
380
  try {
361
- await resolveProviders(container, providers, order);
381
+ await resolveProviders(container, providers, order, control);
362
382
  } catch (error) {
363
383
  logger.error(`${error.message}. The bot cannot start without it.`);
364
384
  logger.debug('Provider failure:', error.cause);
365
385
  markExplained(error);
366
386
  throw error;
367
387
  }
368
- // The listed services are made now, so constructors that attach listeners or connect run before login
388
+ // The listed services are made now, so constructors that attach listeners or connect run before login; a stop
389
+ // meanwhile makes no more of them
369
390
  for (const svc of options.services ?? []){
391
+ if (control.stopped()) return;
370
392
  container.get(svc);
371
393
  }
372
- if (shardingRole(meocordConfig) === 'shard' && container.get(CooldownStore) instanceof MemoryCooldownStore) {
394
+ if (control.stopped()) return;
395
+ if (shard && container.get(CooldownStore) instanceof MemoryCooldownStore) {
373
396
  warnPerShardCooldowns(options.controllers, logger);
374
397
  }
375
398
  bindAppPresenter(container, target, discordClient);
@@ -377,7 +400,7 @@ import { isShardProcess } from '../util/shard-process.util.js';
377
400
  registerClientTranslator(discordClient, options.i18n);
378
401
  };
379
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
380
- options.warnUnanswered ?? process.env.NODE_ENV === 'development');
403
+ options.warnUnanswered ?? process.env.NODE_ENV === 'development', shard);
381
404
  });
382
405
  }
383
406
  }