meocord 4.2.2 → 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 +130 -0
- package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
- package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
- package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
- package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
- package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
- package/dist/cjs/common/index.cjs +4 -104
- package/dist/cjs/core/index.cjs +432 -181
- package/dist/cjs/decorator/index.cjs +85 -120
- package/dist/cjs/testing/index.cjs +1664 -784
- 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 +27 -5
- package/dist/esm/common/sharded-cooldown-store.js +11 -9
- package/dist/esm/common/theme.js +3 -1
- package/dist/esm/common/translator.js +17 -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 +83 -58
- 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 +4 -4
- 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 +44 -28
- 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 +80 -53
- package/dist/esm/core/meocord.app.js +174 -89
- package/dist/esm/core/message-help.js +5 -9
- 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 +2 -1
- package/dist/esm/core/process-state.js +19 -0
- package/dist/esm/core/providers.js +28 -3
- 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/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 +27 -29
- package/dist/esm/core/theme-validation.js +1 -1
- package/dist/esm/decorator/app.decorator.js +6 -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 +2 -2
- 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 +29 -13
- package/dist/esm/testing/cooldown-store-suite.js +3 -3
- package/dist/esm/testing/discord-defaults.js +449 -355
- package/dist/esm/testing/inspect-handler.js +91 -10
- package/dist/esm/testing/meocord-testing-module.js +195 -77
- package/dist/esm/testing/mock-fn.js +8 -2
- package/dist/esm/testing/mock-interaction.js +639 -113
- 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 +1 -0
- 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 +15 -4
- package/dist/types/common/index.d.cts +13 -9
- package/dist/types/common/index.d.ts +13 -9
- 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 +137 -54
- package/dist/types/testing/index.d.ts +137 -54
- 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
|
@@ -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);
|
|
@@ -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 };
|
|
@@ -64,50 +64,50 @@ const merges = new WeakMap();
|
|
|
64
64
|
if (!merged) byLayer.set(layer, merged = deepFreeze(mergeInto(theme, layer)));
|
|
65
65
|
return merged;
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
/**
|
|
68
|
+
* The theme state outside a call, one for both builds of this version so either reads what the other set: the defaults
|
|
69
|
+
* with what the deprecated `Theme` statics set beneath every theme, the app whose theme is read outside a call, and how
|
|
70
|
+
* an answer outside a call finds its theme.
|
|
71
|
+
*/ const state = shared('themeState', ()=>({
|
|
72
|
+
defaults: DEFAULT_THEME,
|
|
73
|
+
legacyVersion: 0,
|
|
74
|
+
ambient: undefined,
|
|
75
|
+
ambientVersion: 0,
|
|
76
|
+
themeOutsideCalls: undefined
|
|
77
|
+
}));
|
|
71
78
|
/** MeoCord's defaults, with what the deprecated `Theme` statics set over them. */ function defaultTheme() {
|
|
72
|
-
return defaults;
|
|
79
|
+
return state.defaults;
|
|
73
80
|
}
|
|
74
81
|
/** The number of times the legacy layer has changed, so a theme built on an older one is built again. */ function themeLayersVersion() {
|
|
75
|
-
return legacyVersion;
|
|
82
|
+
return state.legacyVersion;
|
|
76
83
|
}
|
|
77
84
|
/** Sets the layer the deprecated `Theme` statics write, merged over MeoCord's defaults. */ function setLegacyThemeLayer(layer) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
legacyVersion++;
|
|
85
|
+
state.defaults = mergeTheme(DEFAULT_THEME, layer === undefined ? undefined : copyLayer(layer));
|
|
86
|
+
state.legacyVersion++;
|
|
81
87
|
}
|
|
82
|
-
// The theme outside any call: the one of the app or testing module that claimed it, until it gives it up
|
|
83
|
-
let ambient;
|
|
84
|
-
let ambientVersion = 0;
|
|
85
88
|
/** The number of times the app read outside a call has changed, so each app decides again whether its calls need a scope. */ function ambientThemeVersion() {
|
|
86
|
-
return ambientVersion;
|
|
87
|
-
}
|
|
88
|
-
/** Whether some app's theme is the one read outside a call. */ function hasAmbientTheme() {
|
|
89
|
-
return ambient !== undefined;
|
|
89
|
+
return state.ambientVersion;
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Makes an app's theme the one read outside a call, when no other app has: a bot runs one app, and code it runs
|
|
93
93
|
* outside a handler, such as a scheduled job, then reads that app's theme. A second app in the same process keeps
|
|
94
94
|
* its theme to its own calls.
|
|
95
95
|
*/ function claimAmbientTheme(owner, theme) {
|
|
96
|
-
if (ambient && ambient.owner !== owner) return false;
|
|
97
|
-
if (!ambient) ambientVersion++;
|
|
98
|
-
ambient = {
|
|
96
|
+
if (state.ambient && state.ambient.owner !== owner) return false;
|
|
97
|
+
if (!state.ambient) state.ambientVersion++;
|
|
98
|
+
state.ambient = {
|
|
99
99
|
owner,
|
|
100
100
|
theme
|
|
101
101
|
};
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
104
104
|
/** Gives up the theme read outside a call, when `owner` has it: its start failed, or it has shut down. */ function releaseAmbientTheme(owner) {
|
|
105
|
-
if (ambient?.owner !== owner) return;
|
|
106
|
-
ambient = undefined;
|
|
107
|
-
ambientVersion++;
|
|
105
|
+
if (state.ambient?.owner !== owner) return;
|
|
106
|
+
state.ambient = undefined;
|
|
107
|
+
state.ambientVersion++;
|
|
108
108
|
}
|
|
109
109
|
/** Whether `owner` is the app whose theme is read outside a call. */ function ownsAmbientTheme(owner) {
|
|
110
|
-
return ambient?.owner === owner;
|
|
110
|
+
return state.ambient?.owner === owner;
|
|
111
111
|
}
|
|
112
112
|
/** Runs `fn` with `theme` as the theme of the call, for everything it runs, awaits or starts. */ function runWithTheme(theme, fn) {
|
|
113
113
|
return scope.run({
|
|
@@ -123,10 +123,8 @@ let ambientVersion = 0;
|
|
|
123
123
|
/** Runs `fn` in `themeScope`, whose theme may be replaced while it runs, as a per-server theme is found. */ function runInThemeScope(themeScope, fn) {
|
|
124
124
|
return scope.run(themeScope, fn);
|
|
125
125
|
}
|
|
126
|
-
// Set by the runtime: the theme an app gives an interaction answered outside any call, as in a collector
|
|
127
|
-
let themeOutsideCalls;
|
|
128
126
|
/** Sets how an answer outside any call finds its theme: from the interaction's app, server and user. */ function setThemeOutsideCalls(resolve) {
|
|
129
|
-
themeOutsideCalls = resolve;
|
|
127
|
+
state.themeOutsideCalls = resolve;
|
|
130
128
|
}
|
|
131
129
|
/**
|
|
132
130
|
* The theme an answer to `interaction` uses: the running call's, or, outside any call, as in a collector's callback,
|
|
@@ -134,7 +132,7 @@ let themeOutsideCalls;
|
|
|
134
132
|
*/ function themeForInteraction(interaction) {
|
|
135
133
|
const call = scope.getStore();
|
|
136
134
|
if (call) return call.theme;
|
|
137
|
-
return themeOutsideCalls?.(interaction) ?? useTheme();
|
|
135
|
+
return state.themeOutsideCalls?.(interaction) ?? useTheme();
|
|
138
136
|
}
|
|
139
137
|
/**
|
|
140
138
|
* The theme of the running call, with every role present.
|
|
@@ -163,7 +161,7 @@ let themeOutsideCalls;
|
|
|
163
161
|
*
|
|
164
162
|
* @group Responses
|
|
165
163
|
*/ function useTheme() {
|
|
166
|
-
return scope.getStore()?.theme ?? ambient?.theme() ?? defaults;
|
|
164
|
+
return scope.getStore()?.theme ?? state.ambient?.theme() ?? state.defaults;
|
|
167
165
|
}
|
|
168
166
|
/**
|
|
169
167
|
* Makes a function run in the theme of the call that binds it, wherever it is called from later.
|
|
@@ -198,4 +196,4 @@ let themeOutsideCalls;
|
|
|
198
196
|
};
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
export { ambientThemeVersion, bindTheme, claimAmbientTheme, copyLayer, defaultTheme,
|
|
199
|
+
export { ambientThemeVersion, bindTheme, claimAmbientTheme, copyLayer, defaultTheme, mergeTheme, outsideThemeScope, ownsAmbientTheme, releaseAmbientTheme, runInThemeScope, runWithTheme, setLegacyThemeLayer, setThemeOutsideCalls, themeForInteraction, themeLayersVersion, useTheme };
|
|
@@ -51,7 +51,7 @@ const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an arr
|
|
|
51
51
|
const prototype = Object.getPrototypeOf(value);
|
|
52
52
|
return prototype === Object.prototype || prototype === null;
|
|
53
53
|
};
|
|
54
|
-
const PLAIN = "give a plain object, such as {
|
|
54
|
+
const PLAIN = "give a plain object: a database row's plain form, such as Mongoose's toObject() or Sequelize's get({ plain: true }), or an object literal of the values you set, since anything else would replace the whole";
|
|
55
55
|
/** What a non-plain object is: by its class, or as one without a plain prototype when it has none named. */ const describeInstance = (value)=>{
|
|
56
56
|
const name = value.constructor?.name;
|
|
57
57
|
return typeof name === 'string' && name ? `a ${name}` : 'an object without a plain prototype';
|
|
@@ -8,10 +8,12 @@ import { assertObservers } from '../core/observer-runner.js';
|
|
|
8
8
|
import { assertValidTheme, themeForProblem } from '../core/theme-validation.js';
|
|
9
9
|
import { copyLayer } from '../core/theme-scope.js';
|
|
10
10
|
import { declaring, deprecatedOnMethod, refuse } from '../util/refusal.util.js';
|
|
11
|
-
import { timeoutProblem } from '../util/timer-limit.util.js';
|
|
11
|
+
import { timeoutProblem, timerKeepsSeconds, MAX_TIMER_SECONDS } from '../util/timer-limit.util.js';
|
|
12
|
+
import { Logger } from '../common/logger.js';
|
|
12
13
|
import { isConstructor, describeValue } from '../util/value.util.js';
|
|
13
14
|
import { META } from '../util/metadata-keys.js';
|
|
14
15
|
|
|
16
|
+
const logger = new Logger('MeoCord');
|
|
15
17
|
/** Refuses a `messages` option of the wrong type where the app is declared, rather than at the first message. */ function assertMessageOptions(appName, messages, i18n) {
|
|
16
18
|
if (!messages) return;
|
|
17
19
|
const { prefix, mention, caseSensitive } = messages;
|
|
@@ -39,6 +41,9 @@ import { META } from '../util/metadata-keys.js';
|
|
|
39
41
|
if (deleteUsageRepliesAfter !== undefined && !(typeof deleteUsageRepliesAfter === 'number' && deleteUsageRepliesAfter >= 0 && Number.isFinite(deleteUsageRepliesAfter))) {
|
|
40
42
|
throw refuse(new TypeError(`${appName}: @MeoCord({ messages: { deleteUsageRepliesAfter } }) takes a number of seconds, or 0 to keep usage replies.`));
|
|
41
43
|
}
|
|
44
|
+
if (deleteUsageRepliesAfter !== undefined && !timerKeepsSeconds(deleteUsageRepliesAfter)) {
|
|
45
|
+
logger.warn(`${appName}: @MeoCord({ messages: { deleteUsageRepliesAfter } }) is ${deleteUsageRepliesAfter} seconds, longer than a timer ` + `can wait (${MAX_TIMER_SECONDS}), so usage replies are kept. The next major version (5.0) refuses it.`);
|
|
46
|
+
}
|
|
42
47
|
const { help } = messages;
|
|
43
48
|
// One word each, since help is asked for by its first word
|
|
44
49
|
const isWord = (value)=>typeof value === 'string' && /^\S+$/.test(value);
|
|
@@ -2,7 +2,15 @@ import 'reflect-metadata';
|
|
|
2
2
|
import { makeInjectable } from '../util/injectable.util.js';
|
|
3
3
|
import { META } from '../util/metadata-keys.js';
|
|
4
4
|
import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
5
|
+
import { Logger } from '../common/logger.js';
|
|
5
6
|
|
|
7
|
+
const logger = new Logger('CommandBuilder');
|
|
8
|
+
/** The nearest base class of `target` that gives `guilds`, which `target` inherits when it gives none. */ function guildsBase(target) {
|
|
9
|
+
for(let base = Object.getPrototypeOf(target); base && base !== Function.prototype; base = Object.getPrototypeOf(base)){
|
|
10
|
+
if (Reflect.hasOwnMetadata(META.builderGuilds, base)) return base;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
* Marks a class as a command's builder, which describes the command MeoCord registers with Discord.
|
|
8
16
|
*
|
|
@@ -40,6 +48,12 @@ import { declaring, deprecatedOnMethod } from '../util/refusal.util.js';
|
|
|
40
48
|
if (options.guilds) Reflect.defineMetadata(META.builderGuilds, [
|
|
41
49
|
...options.guilds
|
|
42
50
|
], target);
|
|
51
|
+
else {
|
|
52
|
+
const base = guildsBase(target);
|
|
53
|
+
if (base) {
|
|
54
|
+
logger.warn(`${target.name} gives no guilds and inherits ${base.name}'s; declare guilds on ${target.name} to keep them. ` + 'The next major version (5.0) registers it in the configured scope.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
43
57
|
});
|
|
44
58
|
}
|
|
45
59
|
|