meocord 4.2.0 → 4.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +322 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +188 -169
- package/dist/cjs/decorator/index.cjs +52 -7
- package/dist/cjs/testing/index.cjs +1854 -311
- package/dist/cli.json +1 -1
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +13 -3
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/errors.js +25 -0
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +89 -15
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +52 -11
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +9 -8
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +10 -4
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +59 -34
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-help.js +32 -6
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/providers.js +19 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/shard-context.js +5 -6
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +7 -2
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +31 -0
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +20 -13
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +225 -30
- package/dist/esm/testing/meocord-testing-module.js +158 -44
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1250 -182
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +40 -17
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +2 -2
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +3 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +30 -2
- package/dist/types/common/index.d.cts +10 -7
- package/dist/types/common/index.d.ts +10 -7
- package/dist/types/core/index.d.cts +3 -2
- package/dist/types/core/index.d.ts +3 -2
- package/dist/types/decorator/index.d.cts +1 -0
- package/dist/types/decorator/index.d.ts +1 -0
- package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
- package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
- package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
- package/dist/types/testing/index.d.cts +198 -62
- package/dist/types/testing/index.d.ts +198 -62
- package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
- 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
|
|
46
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
120
|
-
timer = setTimeout(()=>resolve(
|
|
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
|
-
|
|
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
|
-
|
|
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,39 +3,36 @@ 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
|
|
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/
|
|
18
|
-
import '../
|
|
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';
|
|
35
32
|
import { buildMessageRoutes } from './message-routes.js';
|
|
36
33
|
import { appObservers, bindObservers } from './observer-runner.js';
|
|
37
34
|
import { isAppClassToken } from './lifecycle-order.js';
|
|
38
|
-
import { reachableClasses, providerMap, bindsOwnToken, tokenName, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies, resolveProviders } from './providers.js';
|
|
35
|
+
import { reachableClasses, providerMap, assertProvidedServices, bindsOwnToken, tokenName, bindProvider, resolutionOrder, isClassProvider, assertProvided, tokenDependencies, resolveProviders } from './providers.js';
|
|
39
36
|
import { HandlerRegistry } from './handler-registry.js';
|
|
40
37
|
import { ShardManager } from './shard-manager.js';
|
|
41
38
|
import { warnUnregisteredCommands, warnInheritedRoutes, warnOverlappingPatterns, warnHandlersOffControllers, assertDistinctCommands } from './command-conflicts.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' ||
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
...
|
|
156
|
+
...appOptions?.controllers ?? []
|
|
156
157
|
]);
|
|
157
|
-
if (!
|
|
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()));
|
|
@@ -168,7 +176,9 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
168
176
|
const providers = collectStartupErrors(()=>{
|
|
169
177
|
declared.forEach(startupError);
|
|
170
178
|
assertDistinctCommands(options.controllers);
|
|
171
|
-
|
|
179
|
+
const providers = providerMap(options.providers ?? [], '@MeoCord({ providers })');
|
|
180
|
+
assertProvidedServices(typeof target === 'function' ? target.name : String(target), options.services ?? [], providers, '@MeoCord({ providers })');
|
|
181
|
+
return providers;
|
|
172
182
|
});
|
|
173
183
|
// A themeFor class is bound on its own, as the cooldown store is, and resolved like a service
|
|
174
184
|
const themeResolver = themeResolverClass(options.themeFor);
|
|
@@ -185,7 +195,7 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
185
195
|
...appObservers(target)
|
|
186
196
|
];
|
|
187
197
|
// A shard's manager runs the same checks, so a sharded bot warns once
|
|
188
|
-
if (!
|
|
198
|
+
if (!shard) {
|
|
189
199
|
warnUnregisteredCommands(options.controllers);
|
|
190
200
|
warnInheritedRoutes(options.controllers);
|
|
191
201
|
warnOverlappingPatterns(options.controllers);
|
|
@@ -200,11 +210,8 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
200
210
|
if (isRegisterOnly()) {
|
|
201
211
|
return new MeoCordApp(options.controllers, new Container(), new Client(options.clientOptions), meocordConfig.discordToken);
|
|
202
212
|
}
|
|
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
213
|
// A process-sharding manager only spawns shards, so it binds, constructs and connects nothing itself.
|
|
207
|
-
if (shardingRole(meocordConfig) === 'manager') {
|
|
214
|
+
if (shardingRole(meocordConfig, shard) === 'manager') {
|
|
208
215
|
return new ShardManager({
|
|
209
216
|
controllerClasses: options.controllers,
|
|
210
217
|
token: meocordConfig.discordToken,
|
|
@@ -242,7 +249,7 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
242
249
|
], lateGuardCheck(providers, translator));
|
|
243
250
|
bindGlobalStages(container, stages);
|
|
244
251
|
// 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);
|
|
252
|
+
const clientOptions = clientOptionsWithSharding(this.effectiveConfig(meocordConfig, shard), options.clientOptions);
|
|
246
253
|
container.bind(Client).toDynamicValue(()=>new Client(clientOptions)).inSingletonScope();
|
|
247
254
|
if (options.i18n) {
|
|
248
255
|
container.bind(Translator).toConstantValue(options.i18n);
|
|
@@ -270,6 +277,10 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
270
277
|
// After MeoCord's own tokens, which a provider may not replace, and before the app's classes, so
|
|
271
278
|
// a class token that is provided is not also bound as itself
|
|
272
279
|
for (const [token, provider] of providers){
|
|
280
|
+
// Bound later, or for each call, so not caught below; supported until 5.0 refuses it
|
|
281
|
+
if (token === ThemeCache || token === ExecutionContext) {
|
|
282
|
+
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.');
|
|
283
|
+
}
|
|
273
284
|
if (container.isBound(token)) {
|
|
274
285
|
startupError(new Error(`${target.name}: @MeoCord({ providers }) cannot provide ${tokenName(token)}, which MeoCord binds itself.`));
|
|
275
286
|
continue;
|
|
@@ -287,6 +298,9 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
287
298
|
// Observers are services too: bound here so their lifecycle hooks run in dependency order
|
|
288
299
|
const observers = appObservers(target);
|
|
289
300
|
for (const observer of observers)bindDependencies(container, observer, providers);
|
|
301
|
+
// What the stages and presenter inject, bound now so a service among them is made as the bot comes online
|
|
302
|
+
const stageServices = stageDependencies(runs, providers, options.presenter);
|
|
303
|
+
for (const service of stageServices)bindDependencies(container, service, providers);
|
|
290
304
|
// What follows walks what these bound, so a class refused here is not reported again as missing
|
|
291
305
|
stopOnStartupErrors();
|
|
292
306
|
// Providers first, then the services, the controllers and the observers, each after what it depends on. The app's
|
|
@@ -307,12 +321,19 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
307
321
|
return isAppClassToken(token) && (!provider || isClassProvider(provider) && provider.useClass === token);
|
|
308
322
|
}));
|
|
309
323
|
assertProvided(container, providers, runs.classes, '@MeoCord({ providers })');
|
|
324
|
+
// With what the stages inject among them, each after what it depends on, so their hooks run as a listed service's do
|
|
325
|
+
const roots = stageServices.length > 0 ? resolutionOrder(container, providers, [
|
|
326
|
+
...order,
|
|
327
|
+
...stageServices
|
|
328
|
+
], {
|
|
329
|
+
followOwnTokens: false
|
|
330
|
+
}) : order;
|
|
310
331
|
// The store first, after only what it injects: it is ready before anything a call reaches, and shuts down last
|
|
311
332
|
const store = options.cooldownStore;
|
|
312
333
|
const lifecycle = (store ? resolutionOrder(container, providers, [
|
|
313
334
|
store,
|
|
314
|
-
...
|
|
315
|
-
]) :
|
|
335
|
+
...roots
|
|
336
|
+
]) : roots).map((token)=>({
|
|
316
337
|
token,
|
|
317
338
|
name: tokenName(token),
|
|
318
339
|
dependencies: tokenDependencies(container, providers, token),
|
|
@@ -352,24 +373,28 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
352
373
|
for (const cls of runs.classes){
|
|
353
374
|
Reflect.defineMetadata(META.container, container, cls);
|
|
354
375
|
}
|
|
376
|
+
recordInstanceContainers(container, runs.classes);
|
|
355
377
|
prepareThemes(container, appClasses);
|
|
356
378
|
// Run by start() before it logs in: what may inject a provided value is resolved once every factory
|
|
357
379
|
// has made its value, including those that return a promise
|
|
358
380
|
const logger = this.logger;
|
|
359
|
-
const startup = async ()=>{
|
|
381
|
+
const startup = async (control)=>{
|
|
360
382
|
try {
|
|
361
|
-
await resolveProviders(container, providers, order);
|
|
383
|
+
await resolveProviders(container, providers, order, control);
|
|
362
384
|
} catch (error) {
|
|
363
385
|
logger.error(`${error.message}. The bot cannot start without it.`);
|
|
364
386
|
logger.debug('Provider failure:', error.cause);
|
|
365
387
|
markExplained(error);
|
|
366
388
|
throw error;
|
|
367
389
|
}
|
|
368
|
-
// The listed services are made now, so constructors that attach listeners or connect run before login
|
|
390
|
+
// The listed services are made now, so constructors that attach listeners or connect run before login; a stop
|
|
391
|
+
// meanwhile makes no more of them
|
|
369
392
|
for (const svc of options.services ?? []){
|
|
393
|
+
if (control.stopped()) return;
|
|
370
394
|
container.get(svc);
|
|
371
395
|
}
|
|
372
|
-
if (
|
|
396
|
+
if (control.stopped()) return;
|
|
397
|
+
if (shard && container.get(CooldownStore) instanceof MemoryCooldownStore) {
|
|
373
398
|
warnPerShardCooldowns(options.controllers, logger);
|
|
374
399
|
}
|
|
375
400
|
bindAppPresenter(container, target, discordClient);
|
|
@@ -377,7 +402,7 @@ import { isShardProcess } from '../util/shard-process.util.js';
|
|
|
377
402
|
registerClientTranslator(discordClient, options.i18n);
|
|
378
403
|
};
|
|
379
404
|
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');
|
|
405
|
+
options.warnUnanswered ?? process.env.NODE_ENV === 'development', shard);
|
|
381
406
|
});
|
|
382
407
|
}
|
|
383
408
|
}
|