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.
- package/CHANGELOG.md +188 -0
- package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
- package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
- package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
- package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +435 -182
- package/dist/cjs/decorator/index.cjs +116 -121
- package/dist/cjs/testing/index.cjs +1923 -808
- package/dist/cli.json +1 -1
- package/dist/esm/bin/app-template/package.json.template +9 -9
- package/dist/esm/bin/app-template/src/main.ts.template +3 -2
- package/dist/esm/bin/generator.js +6 -4
- package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
- package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
- package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
- package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
- package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
- package/dist/esm/bin/helper/service-generator.helper.js +6 -4
- package/dist/esm/bin/meocord.js +106 -48
- package/dist/esm/build/native-addons.js +75 -19
- package/dist/esm/build/rsbuild-config.js +11 -3
- package/dist/esm/build/typed-externals.js +15 -2
- package/dist/esm/common/cooldown-store.js +38 -14
- package/dist/esm/common/deprecation.js +4 -1
- package/dist/esm/common/execution-context.js +3 -1
- package/dist/esm/common/explained-error.js +7 -3
- package/dist/esm/common/log-level.js +13 -9
- package/dist/esm/common/logger.js +4 -2
- package/dist/esm/common/meocord-text.js +11 -6
- package/dist/esm/common/redis-cooldown-store.js +3 -7
- package/dist/esm/common/response/flags.js +17 -1
- package/dist/esm/common/response/install-context.js +27 -16
- package/dist/esm/common/response/presenter.js +7 -3
- package/dist/esm/common/response/response-state.js +32 -10
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +27 -8
- package/dist/esm/core/autocomplete-routes.js +26 -0
- package/dist/esm/core/command-conflicts.js +15 -7
- package/dist/esm/core/command-registration.js +15 -1
- package/dist/esm/core/cooldown-runner.js +7 -9
- package/dist/esm/core/defer.js +2 -5
- package/dist/esm/core/dispatcher.js +92 -66
- package/dist/esm/core/event-requirements.js +67 -19
- package/dist/esm/core/event-sources.js +100 -0
- package/dist/esm/core/fallback.js +3 -1
- package/dist/esm/core/filter-runner.js +6 -6
- package/dist/esm/core/guard-runner.js +173 -52
- package/dist/esm/core/handler-input.js +24 -3
- package/dist/esm/core/handler-pipeline.js +54 -32
- package/dist/esm/core/handler-registry.js +51 -21
- package/dist/esm/core/input-runner.js +7 -8
- package/dist/esm/core/interceptor-runner.js +4 -5
- package/dist/esm/core/lifecycle-hooks.js +3 -8
- package/dist/esm/core/lifecycle-order.js +1 -15
- package/dist/esm/core/meocord-classes.js +14 -1
- package/dist/esm/core/meocord-factory.js +83 -54
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +37 -15
- package/dist/esm/core/message-params.js +126 -46
- package/dist/esm/core/message-routes.js +225 -45
- package/dist/esm/core/message-words.js +30 -2
- package/dist/esm/core/observer-runner.js +4 -2
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +39 -4
- package/dist/esm/core/scalar-types.js +57 -16
- package/dist/esm/core/shard-context.js +11 -10
- package/dist/esm/core/shard-exit.js +3 -3
- package/dist/esm/core/shard-manager.js +16 -6
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +28 -8
- package/dist/esm/core/theme-resolvers.js +26 -11
- package/dist/esm/core/theme-runtime.js +23 -13
- package/dist/esm/core/theme-scope.js +31 -30
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +37 -1
- package/dist/esm/decorator/command-builder.decorator.js +14 -0
- package/dist/esm/decorator/controller-class.decorator.js +1 -1
- package/dist/esm/decorator/controller.decorator.js +48 -19
- package/dist/esm/decorator/defer.decorator.js +2 -2
- package/dist/esm/decorator/event.decorator.js +4 -5
- package/dist/esm/decorator/filter.decorator.js +4 -4
- package/dist/esm/decorator/guard.decorator.js +4 -61
- package/dist/esm/decorator/inject.decorator.js +4 -1
- package/dist/esm/decorator/interceptor.decorator.js +2 -2
- package/dist/esm/decorator/theme.decorator.js +4 -3
- package/dist/esm/decorator/validation.decorator.js +5 -5
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +32 -25
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -335
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +217 -91
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +864 -124
- package/dist/esm/testing/response-log.js +8 -0
- package/dist/esm/testing/response.js +7 -5
- package/dist/esm/testing/routing.js +48 -6
- package/dist/esm/testing/strict-mocks.js +8 -4
- package/dist/esm/testing/theme.js +3 -2
- package/dist/esm/util/common.util.js +30 -9
- package/dist/esm/util/generator-cli.util.js +6 -4
- package/dist/esm/util/inherited-env.util.js +72 -13
- package/dist/esm/util/json.util.js +10 -3
- package/dist/esm/util/meocord-cli.util.js +3 -2
- package/dist/esm/util/meocord-config-loader.util.js +14 -1
- package/dist/esm/util/meocord-config-validation.util.js +13 -3
- package/dist/esm/util/meocord-source-config.util.js +13 -3
- package/dist/esm/util/platform.util.js +3 -2
- package/dist/esm/util/refusal.util.js +29 -11
- package/dist/esm/util/registration-mode.util.js +14 -1
- package/dist/esm/util/timer-limit.util.js +4 -1
- package/dist/esm/util/tsconfig-paths.util.js +38 -7
- package/dist/esm/util/tsconfig.util.js +16 -58
- package/dist/esm/util/user-text.util.js +8 -4
- package/dist/esm/util/value.util.js +33 -5
- package/dist/types/common/index.d.cts +17 -10
- package/dist/types/common/index.d.ts +17 -10
- package/dist/types/core/index.d.cts +21 -11
- package/dist/types/core/index.d.ts +21 -11
- package/dist/types/decorator/index.d.cts +63 -63
- package/dist/types/decorator/index.d.ts +63 -63
- package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
- package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
- package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
- package/dist/types/testing/index.d.cts +158 -61
- package/dist/types/testing/index.d.ts +158 -61
- package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
- package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
- package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
- package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
- package/package.json +5 -2
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ExecutionContext } from '../common/execution-context.js';
|
|
2
|
+
import { warnOnce } from '../common/deprecation.js';
|
|
3
|
+
import { ThemeCache } from './theme-resolvers.js';
|
|
2
4
|
import { injectedTokens, undecoratedConstructor, untypedParameter } from './guard-runner.js';
|
|
3
|
-
import {
|
|
5
|
+
import { meocordClassAdvice, meocordClasses } from './meocord-classes.js';
|
|
4
6
|
import { isAppClassToken } from './lifecycle-order.js';
|
|
5
7
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
6
8
|
import { startupError, refuse } from '../util/refusal.util.js';
|
|
9
|
+
import { isConstructor } from '../util/value.util.js';
|
|
7
10
|
|
|
8
11
|
const isValueProvider = (provider)=>'useValue' in provider;
|
|
9
12
|
const isClassProvider = (provider)=>'useClass' in provider;
|
|
@@ -14,6 +17,18 @@ const isFactoryProvider = (provider)=>'useFactory' in provider;
|
|
|
14
17
|
return `'${String(token)}'`;
|
|
15
18
|
}
|
|
16
19
|
const isToken = (value)=>typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
|
|
20
|
+
/**
|
|
21
|
+
* Checks one of an app's own `@MeoCord({ providers })` as the bot does. A provider of a token MeoCord binds later, or
|
|
22
|
+
* for each call, is warned about until 5.0 refuses it; one of a token `isOwn` names, which MeoCord binds before the
|
|
23
|
+
* providers, is refused.
|
|
24
|
+
*
|
|
25
|
+
* @returns The refusal, for the caller to report, or `undefined`.
|
|
26
|
+
*/ function appProviderRefusal(appName, token, isOwn, logger) {
|
|
27
|
+
if (token === ThemeCache || token === ExecutionContext) {
|
|
28
|
+
warnOnce(logger, `${appName}: @MeoCord({ providers }) provides ${tokenName(token)}, which MeoCord binds itself; ` + 'the next major version (5.0) refuses it. Remove the provider.');
|
|
29
|
+
}
|
|
30
|
+
return isOwn(token) ? new Error(`${appName}: @MeoCord({ providers }) cannot provide ${tokenName(token)}, which MeoCord binds itself.`) : undefined;
|
|
31
|
+
}
|
|
17
32
|
/**
|
|
18
33
|
* Checks each provider's shape and indexes them by token, reporting each that cannot be bound, such as one with no
|
|
19
34
|
* token, not exactly one of `useValue`, `useClass` and `useFactory`, one that injects `ExecutionContext`, or a token
|
|
@@ -43,7 +58,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
43
58
|
startupError(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
|
|
44
59
|
continue;
|
|
45
60
|
}
|
|
46
|
-
if ('useClass' in entry &&
|
|
61
|
+
if ('useClass' in entry && !isConstructor(entry.useClass)) {
|
|
47
62
|
startupError(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
|
|
48
63
|
continue;
|
|
49
64
|
}
|
|
@@ -73,6 +88,10 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
73
88
|
}
|
|
74
89
|
return map;
|
|
75
90
|
}
|
|
91
|
+
/** For each container, how to forget the value each factory provider made, so a start that failed is made afresh. */ const madeValues = new WeakMap();
|
|
92
|
+
/** Forgets every value `container`'s factory providers made, so the next resolve runs each factory again. */ function forgetFactoryValues(container) {
|
|
93
|
+
for (const forget of madeValues.get(container) ?? [])forget();
|
|
94
|
+
}
|
|
76
95
|
/**
|
|
77
96
|
* Binds one provider into `container` as a singleton. `bindClass` binds a class a provider depends
|
|
78
97
|
* on, as the app binds its own, so a provided class or a factory's `inject` can name any class.
|
|
@@ -93,6 +112,10 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
93
112
|
// Made once, as a singleton, but kept only once made: a factory that throws or rejects runs again on the next
|
|
94
113
|
// resolve, so a start() after it failed tries it again. Resolves while one is pending share it.
|
|
95
114
|
let kept;
|
|
115
|
+
madeValues.set(container, [
|
|
116
|
+
...madeValues.get(container) ?? [],
|
|
117
|
+
()=>kept = undefined
|
|
118
|
+
]);
|
|
96
119
|
container.bind(token).toDynamicValue((context)=>{
|
|
97
120
|
if (kept) return kept.value;
|
|
98
121
|
// Resolved in dependency order before anything needs it, so each injected value is already made
|
|
@@ -120,6 +143,15 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
120
143
|
});
|
|
121
144
|
for (const dependency of inject)if (isAppClassToken(dependency)) bindClass(dependency);
|
|
122
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Reports each service listed by a string or symbol token that no provider provides, which would otherwise be bound
|
|
148
|
+
* as if it were a class.
|
|
149
|
+
*/ function assertProvidedServices(appName, services, providers, where) {
|
|
150
|
+
for (const service of services){
|
|
151
|
+
if (typeof service === 'function' || providers.has(service)) continue;
|
|
152
|
+
startupError(new Error(`${appName}: @MeoCord({ services }) lists ${tokenName(service)}, which nothing provides: add a provider for it to ${where}.`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
123
155
|
/**
|
|
124
156
|
* Reports a class or a factory that asks for a string or symbol token nothing binds, naming both, rather than failing
|
|
125
157
|
* when the class is first resolved. A class token is bound on demand, so it is never missing.
|
|
@@ -255,11 +287,13 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
255
287
|
* Runs every factory in `order`, awaiting those that return a promise, so each value is made before
|
|
256
288
|
* anything that injects it is resolved. Rejects with {@link providerFailure} for the first that fails. With `control`,
|
|
257
289
|
* it makes no further value once the app is stopping.
|
|
290
|
+
*
|
|
291
|
+
* @returns Whether every factory was made: `false` when the app began stopping first.
|
|
258
292
|
*/ async function resolveProviders(container, providers, order, control) {
|
|
259
293
|
for (const token of order){
|
|
260
294
|
const provider = providers.get(token);
|
|
261
295
|
if (!provider || !isFactoryProvider(provider)) continue;
|
|
262
|
-
if (control?.stopped()) return;
|
|
296
|
+
if (control?.stopped()) return false;
|
|
263
297
|
control?.pending(tokenName(token));
|
|
264
298
|
try {
|
|
265
299
|
await container.getAsync(token);
|
|
@@ -269,6 +303,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
|
|
|
269
303
|
control?.pending(undefined);
|
|
270
304
|
}
|
|
271
305
|
}
|
|
306
|
+
return true;
|
|
272
307
|
}
|
|
273
308
|
|
|
274
|
-
export { assertProvided, assertTypedParameters, bindProvider, bindsOwnToken, isClassProvider, isFactoryProvider, isValueProvider, providerFailure, providerMap, reachableClasses, resolutionOrder, resolveProviders, tokenDependencies, tokenName };
|
|
309
|
+
export { appProviderRefusal, assertProvided, assertProvidedServices, assertTypedParameters, bindProvider, bindsOwnToken, forgetFactoryValues, isClassProvider, isFactoryProvider, isValueProvider, providerFailure, providerMap, reachableClasses, resolutionOrder, resolveProviders, tokenDependencies, tokenName };
|
|
@@ -21,34 +21,70 @@ const UNIT_MS = lookupTable({
|
|
|
21
21
|
w: 604800000
|
|
22
22
|
});
|
|
23
23
|
/** A number written in full, `whole` for an integer: `undefined` for anything else. */ function number(word, whole) {
|
|
24
|
-
|
|
24
|
+
// Each run of digits splits only one way, so refusing a word takes time linear in its length
|
|
25
|
+
if (!(whole ? /^[+-]?\d+$/ : /^[+-]?(\d+(?:\.\d*)?|\.\d+)(e[+-]?\d+)?$/i).test(word)) return undefined;
|
|
25
26
|
const value = Number(word);
|
|
26
27
|
return Number.isFinite(value) && (!whole || Number.isSafeInteger(value)) ? value : undefined;
|
|
27
28
|
}
|
|
28
29
|
/** `yes`, `no`, `true`, `false`, `on` or `off`, in any case, as a boolean. */ const bool = (word)=>BOOLEANS[word.toLowerCase()];
|
|
29
|
-
/**
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* A length of time written as amounts and units, such as `90s`, `2h30m` or `1.5d`, in milliseconds: `undefined` for one
|
|
32
|
+
* too long to add up.
|
|
33
|
+
*/ function duration(word) {
|
|
34
|
+
// Sticky: each part starts where the last ended, so the scan stops at the first that doesn't and reads each part once
|
|
35
|
+
const parts = word.toLowerCase().match(/(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/gy);
|
|
31
36
|
if (!parts || parts.join('') !== word.toLowerCase()) return undefined;
|
|
32
|
-
|
|
37
|
+
const total = parts.reduce((sum, part)=>{
|
|
33
38
|
const [, amount, unit] = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(part);
|
|
34
|
-
return
|
|
39
|
+
return sum + Number(amount) * UNIT_MS[unit];
|
|
35
40
|
}, 0);
|
|
41
|
+
// Too long to add up, as number() refuses a value past the largest number
|
|
42
|
+
return Number.isFinite(total) ? total : undefined;
|
|
36
43
|
}
|
|
37
44
|
/** The largest snowflake: Discord's IDs are unsigned 64-bit integers. */ const MAX_SNOWFLAKE = 2n ** 64n - 1n;
|
|
45
|
+
/** The fewest and most digits a snowflake has, and the words of that many digits, with no leading zero. */ const SNOWFLAKE_DIGITS = {
|
|
46
|
+
min: 17,
|
|
47
|
+
max: 20
|
|
48
|
+
};
|
|
49
|
+
const SNOWFLAKE = new RegExp(`^[1-9]\\d{${SNOWFLAKE_DIGITS.min - 1},${SNOWFLAKE_DIGITS.max - 1}}$`);
|
|
38
50
|
/**
|
|
39
51
|
* A Discord ID, kept as its text: 17 to 20 digits, no leading zero, up to the largest 64-bit value. An ID's top 42 bits
|
|
40
52
|
* are milliseconds since 2015-01-01, so every ID made from 2015-01-28 on has 17 digits or more, and none is a safe
|
|
41
53
|
* integer.
|
|
42
|
-
*/ const snowflake = (word)
|
|
43
|
-
/** A UUID
|
|
54
|
+
*/ const snowflake = (word)=>SNOWFLAKE.test(word) && BigInt(word) <= MAX_SNOWFLAKE ? word : undefined;
|
|
55
|
+
/** A UUID's canonical 8-4-4-4-12 form, 36 characters with its dashes. */ const UUID = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
|
|
56
|
+
const UUID_LENGTH = 36;
|
|
57
|
+
/** A UUID in its canonical 8-4-4-4-12 form, in either case, kept as written. */ const uuid = (word)=>UUID.test(word) ? word : undefined;
|
|
44
58
|
/** The words a type such as `on|off` chooses from, or `undefined` for any other type. */ const choicesOf = (type)=>type.includes('|') ? type.split('|') : undefined;
|
|
45
|
-
/** The
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
/** The shortest segment of any type: one character. */ const SHORTEST_SEGMENT = 1;
|
|
60
|
+
/**
|
|
61
|
+
* The types a component's customId segment can take: each parsed from the segment alone, with no request, and the
|
|
62
|
+
* fewest characters a segment of it has, which the check for a pattern no customId can match counts.
|
|
63
|
+
*/ const SEGMENT_TYPES = lookupTable({
|
|
64
|
+
string: {
|
|
65
|
+
parse: (segment)=>segment,
|
|
66
|
+
shortest: SHORTEST_SEGMENT
|
|
67
|
+
},
|
|
68
|
+
int: {
|
|
69
|
+
parse: (segment)=>number(segment, true),
|
|
70
|
+
shortest: SHORTEST_SEGMENT
|
|
71
|
+
},
|
|
72
|
+
number: {
|
|
73
|
+
parse: (segment)=>number(segment, false),
|
|
74
|
+
shortest: SHORTEST_SEGMENT
|
|
75
|
+
},
|
|
76
|
+
bool: {
|
|
77
|
+
parse: bool,
|
|
78
|
+
shortest: Math.min(...Object.keys(BOOLEANS).map((word)=>word.length))
|
|
79
|
+
},
|
|
80
|
+
snowflake: {
|
|
81
|
+
parse: snowflake,
|
|
82
|
+
shortest: SNOWFLAKE_DIGITS.min
|
|
83
|
+
},
|
|
84
|
+
uuid: {
|
|
85
|
+
parse: uuid,
|
|
86
|
+
shortest: UUID_LENGTH
|
|
87
|
+
}
|
|
52
88
|
});
|
|
53
89
|
/**
|
|
54
90
|
* Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, `snowflake`, `uuid`, or words to
|
|
@@ -57,7 +93,12 @@ const UNIT_MS = lookupTable({
|
|
|
57
93
|
/** A customId segment as its type's value, words to choose from compared as written: `undefined` when it is not one. */ function parseSegment(type, segment) {
|
|
58
94
|
const choices = choicesOf(type);
|
|
59
95
|
if (choices) return choices.includes(segment) ? segment : undefined;
|
|
60
|
-
return SEGMENT_TYPES[type](segment);
|
|
96
|
+
return SEGMENT_TYPES[type].parse(segment);
|
|
97
|
+
}
|
|
98
|
+
/** The fewest characters a customId segment of `type` has: words to choose from count their shortest word. */ function shortestSegment(type) {
|
|
99
|
+
const choices = choicesOf(type);
|
|
100
|
+
if (choices) return Math.min(...choices.map((word)=>word.length));
|
|
101
|
+
return SEGMENT_TYPES[type]?.shortest ?? SHORTEST_SEGMENT;
|
|
61
102
|
}
|
|
62
103
|
|
|
63
|
-
export { bool, choicesOf, duration, isSegmentType, lookupTable, number, parseSegment, snowflake, uuid };
|
|
104
|
+
export { bool, choicesOf, duration, isSegmentType, lookupTable, number, parseSegment, shortestSegment, snowflake, uuid };
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { Logger } from '../common/logger.js';
|
|
3
|
-
import '
|
|
4
|
-
import './theme-validation.js';
|
|
3
|
+
import '../common/theme.js';
|
|
5
4
|
import '../common/decorator.js';
|
|
6
5
|
import 'reflect-metadata';
|
|
7
6
|
import 'discord.js';
|
|
7
|
+
import '../common/deprecation.js';
|
|
8
8
|
import '../common/response/response-state.js';
|
|
9
|
+
import './theme-scope.js';
|
|
9
10
|
import '../common/errors.js';
|
|
11
|
+
import '../common/cooldown-store.js';
|
|
10
12
|
import '../decorator/controller.decorator.js';
|
|
11
13
|
import './scalar-types.js';
|
|
12
|
-
import '
|
|
13
|
-
import 'node:fs';
|
|
14
|
-
import 'node:url';
|
|
14
|
+
import '../util/refusal.util.js';
|
|
15
15
|
import '../common/translator.js';
|
|
16
16
|
import '../common/meocord-text.js';
|
|
17
|
+
import './theme-resolvers.js';
|
|
17
18
|
|
|
18
19
|
/** Where each shard's client keeps the function that runs a `ShardContext.call` in that shard. */ const SHARD_CALL_KEY = Symbol.for('meocord.shardCall');
|
|
19
20
|
const packArgs = (args)=>Object.assign({
|
|
@@ -122,11 +123,11 @@ async function withTimeout(promise, ms, what) {
|
|
|
122
123
|
*
|
|
123
124
|
* With process sharding that is one result per shard; with one process, a single result listing every
|
|
124
125
|
* shard. The arguments and the result pass as JSON in every mode, one process and tests included, so a
|
|
125
|
-
* `Date` arrives as a string
|
|
126
|
-
* `BigInt`, fails the call. A method whose params JSON would
|
|
127
|
-
* the argument is refused, naming what to declare instead. An
|
|
128
|
-
* that throws, lacks the service, or does not answer within
|
|
129
|
-
* others.
|
|
126
|
+
* `Date` arrives as a string, a `Map` as `{}` and a class instance as its own fields, without its getters, wherever
|
|
127
|
+
* it runs, and a value JSON cannot carry, such as a `BigInt`, fails the call. A method whose params JSON would
|
|
128
|
+
* change, such as one taking a `Date`, cannot be called: the argument is refused, naming what to declare instead. An
|
|
129
|
+
* `undefined` argument arrives as `undefined`. A process that throws, lacks the service, or does not answer within
|
|
130
|
+
* 10 seconds gives an error result instead of failing the others.
|
|
130
131
|
*
|
|
131
132
|
* @param service - The controller, the service, or a class a provider stands in for; each process resolves its own
|
|
132
133
|
* instance.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isExplainedError, markExplained } from '../common/explained-error.js';
|
|
2
|
+
import { processState } from './process-state.js';
|
|
2
3
|
import { REFUSED_CODE } from './shard-messages.js';
|
|
3
4
|
import { sendToParent } from '../util/parent-send.util.js';
|
|
4
5
|
import { isRefusal, describeRefusal, handOffRefusal } from '../util/refusal.util.js';
|
|
@@ -9,14 +10,13 @@ import { isRefusal, describeRefusal, handOffRefusal } from '../util/refusal.util
|
|
|
9
10
|
*/ async function tellManager(message) {
|
|
10
11
|
await sendToParent(message);
|
|
11
12
|
}
|
|
12
|
-
/** Whether this shard is already ending, so a failure reported twice, as one going on uncaught is, ends it once. */ let ending = false;
|
|
13
13
|
/**
|
|
14
14
|
* Ends a shard whose startup failed, which its manager restarts unless it was told the failure is one every shard
|
|
15
15
|
* meets: a refusal, told here first, which the manager logs before stopping them all, or a login Discord refused, told
|
|
16
16
|
* as it failed. The exit waits until the caller has handled the error and the manager has the reason.
|
|
17
17
|
*/ function endFailedShard(error) {
|
|
18
|
-
if (
|
|
19
|
-
|
|
18
|
+
if (processState.shardEnding) return;
|
|
19
|
+
processState.shardEnding = true;
|
|
20
20
|
const refused = isRefusal(error);
|
|
21
21
|
const reason = refused && describeRefusal(error, process.cwd());
|
|
22
22
|
if (refused) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { fetchRecommendedShardCount, ShardingManager, REST, Routes } from 'discord.js';
|
|
2
2
|
import { Logger, hideInLogs } from '../common/logger.js';
|
|
3
|
-
import '
|
|
4
|
-
import './theme-validation.js';
|
|
3
|
+
import '../common/theme.js';
|
|
5
4
|
import '../common/decorator.js';
|
|
6
5
|
import 'reflect-metadata';
|
|
6
|
+
import '../common/deprecation.js';
|
|
7
7
|
import '../common/response/response-state.js';
|
|
8
|
+
import './theme-scope.js';
|
|
8
9
|
import '../common/errors.js';
|
|
9
10
|
import { MemoryCooldownStore } from '../common/cooldown-store.js';
|
|
10
11
|
import 'node:crypto';
|
|
@@ -13,10 +14,11 @@ import './scalar-types.js';
|
|
|
13
14
|
import { answerCooldown } from '../common/sharded-cooldown-store.js';
|
|
14
15
|
import '../common/translator.js';
|
|
15
16
|
import '../common/meocord-text.js';
|
|
17
|
+
import './theme-resolvers.js';
|
|
16
18
|
import { registerCommands } from './command-registration.js';
|
|
17
19
|
import { SHUTDOWN_MARGIN_MS, shutdownTimeoutOf } from '../util/shutdown-timeout.util.js';
|
|
18
20
|
import { bundleEntry } from '../util/bundle-entry.util.js';
|
|
19
|
-
import { FORCE_REGISTER_ENV } from '../util/registration-mode.util.js';
|
|
21
|
+
import { FORCE_REGISTER_ENV, registersAsDevelopment } from '../util/registration-mode.util.js';
|
|
20
22
|
import { isShardMessage, REFUSED_CODE } from './shard-messages.js';
|
|
21
23
|
import { tokenMessage, isRefusedToken } from './login-failure.js';
|
|
22
24
|
import { onDevRunnerStop, tellDevRunner, underDevRunner } from '../util/dev-runner.util.js';
|
|
@@ -54,7 +56,10 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
54
56
|
return this.exit(1);
|
|
55
57
|
}
|
|
56
58
|
this.logger.log('Starting shards in separate processes...');
|
|
57
|
-
|
|
59
|
+
const registered = await this.register();
|
|
60
|
+
// A stop that came while it waited ends the start here: nothing after it asks Discord, spawns, listens or exits
|
|
61
|
+
if (this.stopping) return;
|
|
62
|
+
if (!registered) return this.exitForLogin();
|
|
58
63
|
process.on('SIGINT', ()=>void this.stopAndExit());
|
|
59
64
|
process.on('SIGTERM', ()=>void this.stopAndExit());
|
|
60
65
|
// How `meocord start --dev` stops the bot to restart it; no stop request of the user's, whose first Ctrl+C joins it
|
|
@@ -64,6 +69,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
64
69
|
const shards = config.sharding?.shards ?? 'auto';
|
|
65
70
|
total = shards === 'auto' ? await (this.options.recommendedShardCount ?? fetchRecommendedShardCount)(token) : shards;
|
|
66
71
|
} catch (error) {
|
|
72
|
+
if (this.stopping) return;
|
|
67
73
|
if (isRefusedToken(error)) {
|
|
68
74
|
this.logger.error(tokenMessage(token));
|
|
69
75
|
this.logger.debug('Asking for the shard count failed:', error);
|
|
@@ -72,6 +78,7 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
72
78
|
}
|
|
73
79
|
return this.exitForLogin();
|
|
74
80
|
}
|
|
81
|
+
if (this.stopping) return;
|
|
75
82
|
const manager = (this.options.createManager ?? ((path, opts)=>new ShardingManager(path, opts)))(file, {
|
|
76
83
|
token,
|
|
77
84
|
totalShards: total,
|
|
@@ -105,13 +112,15 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
105
112
|
try {
|
|
106
113
|
const rest = this.options.createRest?.(token) ?? new REST().setToken(token);
|
|
107
114
|
const applicationId = (await rest.get(Routes.currentApplication())).id;
|
|
115
|
+
// Stopped while it asked: the commands are left as they are
|
|
116
|
+
if (this.stopping) return true;
|
|
108
117
|
await registerCommands({
|
|
109
118
|
rest,
|
|
110
119
|
applicationId,
|
|
111
120
|
controllerClasses,
|
|
112
121
|
logger: this.logger,
|
|
113
122
|
config: config.commands,
|
|
114
|
-
development:
|
|
123
|
+
development: registersAsDevelopment(),
|
|
115
124
|
force: process.env[FORCE_REGISTER_ENV] === '1'
|
|
116
125
|
});
|
|
117
126
|
} catch (error) {
|
|
@@ -153,7 +162,8 @@ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
153
162
|
if (this.now() - state.spawnedAt >= RESPAWN_RESET_MS) state.attempts = 0;
|
|
154
163
|
const delay = Math.min(RESPAWN_BASE_MS * 2 ** state.attempts, RESPAWN_CAP_MS);
|
|
155
164
|
state.attempts++;
|
|
156
|
-
|
|
165
|
+
const how = child?.signalCode ? `on ${child.signalCode}` : `with code ${child?.exitCode ?? 'unknown'}`;
|
|
166
|
+
this.logger.warn(`Shard ${shard.id} exited ${how}; restarting it in ${delay} ms.`);
|
|
157
167
|
state.timer = setTimeout(()=>{
|
|
158
168
|
state.timer = undefined;
|
|
159
169
|
if (!this.stopping && !this.fatal) void this.spawn(shard);
|
|
@@ -46,7 +46,8 @@ import { META } from '../util/metadata-keys.js';
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
/** The class of a stage entry. */ function stageClass(entry) {
|
|
49
|
-
|
|
49
|
+
// Entries were checked as they were declared, so a function is the class itself
|
|
50
|
+
return typeof entry === 'function' ? entry : entry.provide;
|
|
50
51
|
}
|
|
51
52
|
/** The context types a stage declared, or `undefined` when it runs for every type. */ function stageTypes(entry) {
|
|
52
53
|
return Reflect.getMetadata(META.stageTypes, stageClass(entry));
|
|
@@ -5,6 +5,7 @@ import { reachableClasses, assertTypedParameters } from './providers.js';
|
|
|
5
5
|
import { themeResolverClass } from './theme-resolvers.js';
|
|
6
6
|
import { isAppClassToken } from './lifecycle-order.js';
|
|
7
7
|
import { ExecutionContext } from '../common/execution-context.js';
|
|
8
|
+
import { getEventHandlers } from '../decorator/event.decorator.js';
|
|
8
9
|
import { META } from '../util/metadata-keys.js';
|
|
9
10
|
import { startupError } from '../util/refusal.util.js';
|
|
10
11
|
|
|
@@ -63,19 +64,38 @@ import { startupError } from '../util/refusal.util.js';
|
|
|
63
64
|
seen.add(cls);
|
|
64
65
|
return injectedTokens(cls).some((token)=>token === ExecutionContext || isAppClassToken(token) && !providers.has(token) && !seen.has(token) && reachesContext(token, providers, seen));
|
|
65
66
|
}
|
|
66
|
-
/**
|
|
67
|
-
* The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
|
|
68
|
-
* app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
|
|
69
|
-
* made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
|
|
70
|
-
* is not made.
|
|
71
|
-
*/ function stageDependencies({ decorators }, providers, presenter) {
|
|
67
|
+
/** The app's classes the stages and presenter inject. */ function stageInjected({ decorators }, presenter) {
|
|
72
68
|
const stages = [
|
|
73
69
|
...decorators
|
|
74
70
|
].filter(([, decorator])=>decorator !== '@Controller()').map(([stage])=>stage);
|
|
75
71
|
if (typeof presenter === 'function') stages.push(presenter);
|
|
76
72
|
return [
|
|
77
73
|
...new Set(stages.flatMap((stage)=>injectedTokens(stage)))
|
|
78
|
-
].filter(isAppClassToken)
|
|
74
|
+
].filter(isAppClassToken);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The classes the app's stages and presenter inject, which they would make only at their first call. Made with the
|
|
78
|
+
* app's own classes, a service among them gets its lifecycle hooks. One that reaches the call's `ExecutionContext` is
|
|
79
|
+
* made for each call, so it is left out, and a stage a test replaces is not among the stages, so what it would inject
|
|
80
|
+
* is not made.
|
|
81
|
+
*/ function stageDependencies(startup, providers, presenter) {
|
|
82
|
+
return stageInjected(startup, presenter).filter((cls)=>!reachesContext(cls, providers));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A warning for each `@On` and `@Once` handler on a class the stages and presenter reach that is made for each call,
|
|
86
|
+
* as it reaches `ExecutionContext`: no one instance of it receives events.
|
|
87
|
+
*/ function perCallEventWarnings(startup, providers, presenter) {
|
|
88
|
+
const perCall = new Set();
|
|
89
|
+
// What a class made for each call injects is made for each call too only if it also reaches the context
|
|
90
|
+
const visit = (cls)=>{
|
|
91
|
+
if (perCall.has(cls) || providers.has(cls) || !reachesContext(cls, providers)) return;
|
|
92
|
+
perCall.add(cls);
|
|
93
|
+
injectedTokens(cls).filter(isAppClassToken).forEach(visit);
|
|
94
|
+
};
|
|
95
|
+
stageInjected(startup, presenter).forEach(visit);
|
|
96
|
+
return [
|
|
97
|
+
...perCall
|
|
98
|
+
].flatMap((cls)=>getEventHandlers(cls.prototype).map(({ event, method, once })=>`${cls.name}.${method}: @${once ? 'Once' : 'On'}('${event}') is on a class made for each call, as it injects ` + 'ExecutionContext or a class that does, so it receives no events. The next major version (5.0) refuses it; ' + 'move the handler to a service that does not.'));
|
|
79
99
|
}
|
|
80
100
|
/** How a container checks a guard first met at a call: the guard and what it injects, as the startup checks would. */ function lateGuardCheck(providers, translator) {
|
|
81
101
|
return (guard)=>assertStartupClasses({
|
|
@@ -98,4 +118,4 @@ import { startupError } from '../util/refusal.util.js';
|
|
|
98
118
|
for (const needsOne of classes.filter((cls)=>injectedTokens(cls).includes(Translator)))startupError(missingTranslatorError(needsOne));
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
export { assertStartupClasses, lateGuardCheck, stageDependencies, startupClasses };
|
|
121
|
+
export { assertStartupClasses, lateGuardCheck, perCallEventWarnings, stageDependencies, startupClasses };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { User } from 'discord.js';
|
|
1
2
|
import { Logger } from '../common/logger.js';
|
|
2
3
|
import { themeProblems } from './theme-validation.js';
|
|
3
4
|
import { outsideThemeScope, copyLayer } from './theme-scope.js';
|
|
5
|
+
import { quoteForLog } from '../util/user-text.util.js';
|
|
6
|
+
import { shared } from '../util/shared-state.util.js';
|
|
4
7
|
|
|
5
8
|
/** How long a resolver's result is kept, in seconds, unless `themeCache.ttlSeconds` says otherwise. */ const DEFAULT_THEME_TTL_SECONDS = 300;
|
|
6
9
|
/** How many servers' results are kept, unless `themeCache.maxGuilds` says otherwise. */ const DEFAULT_THEME_MAX_GUILDS = 10000;
|
|
@@ -113,15 +116,25 @@ const logger = new Logger('Theme');
|
|
|
113
116
|
]).finally(()=>clearTimeout(timer));
|
|
114
117
|
}
|
|
115
118
|
/**
|
|
116
|
-
* A result as a layer: copied, then checked; one with problems is left out, with a
|
|
117
|
-
* `null`, as a database gives for a missing row, is no theme, as `undefined` is.
|
|
119
|
+
* A result as a layer: copied, then checked; one with problems, or that throws as it is read, is left out, with a
|
|
120
|
+
* warning once per id when `report`. `null`, as a database gives for a missing row, is no theme, as `undefined` is.
|
|
118
121
|
*/ accept(id, result, report) {
|
|
119
122
|
if (result === undefined || result === null) return undefined;
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
const where = `themeFor.${this.kind} for ${this.kind} ${id}`;
|
|
124
|
+
let layer;
|
|
125
|
+
let problems;
|
|
126
|
+
try {
|
|
127
|
+
layer = copyLayer(result);
|
|
128
|
+
// A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
|
|
129
|
+
problems = themeProblems(layer, where, {
|
|
130
|
+
forLog: true
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// A cycle overflows the copy, and a getter may throw: the result is left out as one with problems is
|
|
134
|
+
problems = [
|
|
135
|
+
`${where}: theme cannot be read: ${quoteForLog(String(error?.message ?? error))}`
|
|
136
|
+
];
|
|
137
|
+
}
|
|
125
138
|
if (problems.length === 0) {
|
|
126
139
|
if (report) this.warned.delete(id);
|
|
127
140
|
return layer;
|
|
@@ -250,13 +263,14 @@ const logger = new Logger('Theme');
|
|
|
250
263
|
const idOf = (value)=>typeof value === 'string' ? value : undefined;
|
|
251
264
|
/**
|
|
252
265
|
* The server and user a call comes from: an interaction's or a message's server and its user or author; a
|
|
253
|
-
* reaction's message's server and the user who reacted, which a reaction handler gets second
|
|
254
|
-
* server and user, when it has them.
|
|
266
|
+
* reaction's message's server and the user who reacted, which a reaction handler gets second as `{ user }` and a
|
|
267
|
+
* reaction event as the `User`; an event argument's server and user, when it has them.
|
|
255
268
|
*/ function themeTargets(args) {
|
|
256
269
|
const [first, second] = args;
|
|
257
270
|
if (!first || typeof first !== 'object') return {};
|
|
258
271
|
const guildId = idOf(first.guildId) ?? idOf(first.guild?.id) ?? idOf(first.message?.guildId);
|
|
259
|
-
|
|
272
|
+
// Only a User's own id: an event of an old and a new channel, role or message would give the new one's otherwise
|
|
273
|
+
const userId = idOf(first.user?.id) ?? idOf(first.author?.id) ?? idOf(second?.user?.id) ?? (second instanceof User ? idOf(second.id) : undefined);
|
|
260
274
|
return {
|
|
261
275
|
guildId,
|
|
262
276
|
userId
|
|
@@ -283,7 +297,8 @@ const idOf = (value)=>typeof value === 'string' ? value : undefined;
|
|
|
283
297
|
user
|
|
284
298
|
];
|
|
285
299
|
}
|
|
286
|
-
|
|
300
|
+
// One per process for this meocord version, as the rest of an app's theme state is
|
|
301
|
+
const stores = shared('themeCaches', ()=>new WeakMap());
|
|
287
302
|
/**
|
|
288
303
|
* An app's cache of the themes `@MeoCord({ themeFor })` looked up, by server and by user.
|
|
289
304
|
*
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import { stageClasses,
|
|
2
|
+
import { stageClasses, stageOwners } from './guard-runner.js';
|
|
3
3
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
4
4
|
import { lookupLayers, resolverCaches, ThemeCache, attachThemeCache } from './theme-resolvers.js';
|
|
5
|
-
import { releaseAmbientTheme, claimAmbientTheme,
|
|
5
|
+
import { setThemeOutsideCalls, releaseAmbientTheme, claimAmbientTheme, defaultTheme, mergeTheme, themeLayersVersion, ambientThemeVersion, ownsAmbientTheme, useTheme } from './theme-scope.js';
|
|
6
6
|
import { META } from '../util/metadata-keys.js';
|
|
7
|
+
import { shared } from '../util/shared-state.util.js';
|
|
7
8
|
|
|
8
|
-
/** Classes with a `@UseTheme`, on the class or one of its methods. */ const THEMED_CLASSES = new WeakSet();
|
|
9
|
-
|
|
9
|
+
/** Classes with a `@UseTheme`, on the class or one of its methods, from either build of this version. */ const THEMED_CLASSES = shared('themedClasses', ()=>new WeakSet());
|
|
10
|
+
// One per process for this meocord version, so an app started through one build finds its theme from the other
|
|
11
|
+
const apps = shared('appThemes', ()=>new WeakMap());
|
|
10
12
|
/** The classes on `cls`'s prototype chain, itself first. */ function chainOf(cls) {
|
|
11
13
|
const chain = [];
|
|
12
14
|
for(let prototype = cls.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
|
|
@@ -37,6 +39,13 @@ const apps = new WeakMap();
|
|
|
37
39
|
}
|
|
38
40
|
attachThemeCache(container.get(ThemeCache), resolvers);
|
|
39
41
|
}
|
|
42
|
+
/** Counts `classes`, whose handlers are prepared once the app's providers are made, among those a theme can vary by. */ function prepareLateThemes(container, classes) {
|
|
43
|
+
const themes = apps.get(container);
|
|
44
|
+
if (!themes || themes.varies || !classes.some((cls)=>chainOf(cls).some((link)=>THEMED_CLASSES.has(link)))) return;
|
|
45
|
+
themes.varies = true;
|
|
46
|
+
// Built again on its next read, with each handler's own theme
|
|
47
|
+
themes.version = -1;
|
|
48
|
+
}
|
|
40
49
|
/** The app's themes, built again when the layers beneath the app's have changed. */ function current(container) {
|
|
41
50
|
const themes = apps.get(container);
|
|
42
51
|
if (themes && themes.version !== themeLayersVersion()) {
|
|
@@ -51,13 +60,12 @@ const apps = new WeakMap();
|
|
|
51
60
|
}
|
|
52
61
|
/**
|
|
53
62
|
* Decides whether the app's calls need a scope, again whenever the layers beneath it or the app read outside a call
|
|
54
|
-
* change. They do when the handlers can differ in theme. Otherwise a call reads the theme outside a call, so it
|
|
55
|
-
*
|
|
56
|
-
*
|
|
63
|
+
* change. They do when the handlers can differ in theme. Otherwise a call reads the theme outside a call, so it needs
|
|
64
|
+
* one unless its app owns that theme: another app may claim it while the call runs, and the call keeps the theme it
|
|
65
|
+
* started with.
|
|
57
66
|
*/ function rescope(container, themes) {
|
|
58
67
|
themes.ambientVersion = ambientThemeVersion();
|
|
59
|
-
|
|
60
|
-
themes.scoped = themes.varies || !readsOwnTheme ? themes.app : undefined;
|
|
68
|
+
themes.scoped = themes.varies || !ownsAmbientTheme(container) ? themes.app : undefined;
|
|
61
69
|
}
|
|
62
70
|
/** The app's theme: MeoCord's defaults and the app's `@MeoCord({ theme })`, or the defaults for a container with none. */ function appTheme(container) {
|
|
63
71
|
return current(container)?.app ?? defaultTheme();
|
|
@@ -75,8 +83,10 @@ const apps = new WeakMap();
|
|
|
75
83
|
for (const cls of stageClasses(prototype, methodName)){
|
|
76
84
|
theme = mergeTheme(theme, Reflect.getOwnMetadata(META.classTheme, cls));
|
|
77
85
|
}
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
// The method's layer over its class's, and an override's over the method it overrides
|
|
87
|
+
for (const owner of stageOwners(prototype, methodName)){
|
|
88
|
+
theme = mergeTheme(theme, Reflect.getOwnMetadata(META.methodTheme, owner, methodName));
|
|
89
|
+
}
|
|
80
90
|
byMethod.set(methodName, theme);
|
|
81
91
|
return theme;
|
|
82
92
|
}
|
|
@@ -115,7 +125,7 @@ const apps = new WeakMap();
|
|
|
115
125
|
ready
|
|
116
126
|
};
|
|
117
127
|
}
|
|
118
|
-
const clientApps = new WeakMap();
|
|
128
|
+
const clientApps = shared('clientThemes', ()=>new WeakMap());
|
|
119
129
|
/** Records which app a client's interactions come to, so an answer outside any call can find its theme. */ function registerClientTheme(client, container) {
|
|
120
130
|
clientApps.set(client, container);
|
|
121
131
|
}
|
|
@@ -139,4 +149,4 @@ setThemeOutsideCalls((interaction)=>{
|
|
|
139
149
|
releaseAmbientTheme(container);
|
|
140
150
|
}
|
|
141
151
|
|
|
142
|
-
export { THEMED_CLASSES, appTheme, beginCallTheme, claimAmbientAppTheme, configureThemes, registerClientTheme, releaseAmbientAppTheme };
|
|
152
|
+
export { THEMED_CLASSES, appTheme, beginCallTheme, claimAmbientAppTheme, configureThemes, prepareLateThemes, registerClientTheme, releaseAmbientAppTheme };
|