meocord 4.1.0-beta.7 → 4.1.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/cjs/_shared/{providers-CIA3ykpw.cjs → providers-CSLRsJOZ.cjs} +56 -26
  3. package/dist/cjs/_shared/{shard-context-fX-JKqOi.cjs → shard-context-DV-BymqL.cjs} +195 -74
  4. package/dist/cjs/_shared/{explained-error-BRrfk1lT.cjs → sharded-cooldown-store-CBKsDtvc.cjs} +1 -42
  5. package/dist/cjs/_shared/{theme-resolvers-B4BV5Ikl.cjs → theme-resolvers-CVm6n5-Q.cjs} +343 -170
  6. package/dist/cjs/_shared/{translate-error-DtdMVhf7.cjs → translate-error-Bts5vGXt.cjs} +7 -6
  7. package/dist/cjs/common/index.cjs +6 -5
  8. package/dist/cjs/core/index.cjs +78 -35
  9. package/dist/cjs/decorator/index.cjs +70 -54
  10. package/dist/cjs/testing/index.cjs +225 -40
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/README.md.template +6 -1
  13. package/dist/esm/bin/app-template/package.json.template +1 -0
  14. package/dist/esm/bin/app-template/src/main.ts.template +3 -2
  15. package/dist/esm/bin/meocord.js +59 -25
  16. package/dist/esm/build/rsbuild-config.js +9 -1
  17. package/dist/esm/build/stack-remap.js +3 -4
  18. package/dist/esm/common/decorator.js +7 -5
  19. package/dist/esm/common/explained-error.js +8 -5
  20. package/dist/esm/common/meocord-messages.js +4 -0
  21. package/dist/esm/common/response/response-state.js +32 -18
  22. package/dist/esm/common/response/send-failure.js +11 -0
  23. package/dist/esm/common/translator.js +4 -3
  24. package/dist/esm/core/command-conflicts.js +3 -2
  25. package/dist/esm/core/cooldown-runner.js +32 -3
  26. package/dist/esm/core/defer.js +1 -1
  27. package/dist/esm/core/dispatcher.js +6 -4
  28. package/dist/esm/core/fallback.js +166 -60
  29. package/dist/esm/core/filter-runner.js +2 -1
  30. package/dist/esm/core/guard-runner.js +1 -1
  31. package/dist/esm/core/handler-pipeline.js +24 -9
  32. package/dist/esm/core/interceptor-runner.js +2 -1
  33. package/dist/esm/core/meocord-factory.js +24 -13
  34. package/dist/esm/core/meocord.app.js +38 -5
  35. package/dist/esm/core/message-routes.js +3 -2
  36. package/dist/esm/core/observer-runner.js +2 -1
  37. package/dist/esm/core/providers.js +13 -12
  38. package/dist/esm/core/shard-manager.js +1 -1
  39. package/dist/esm/core/stage-scope.js +4 -3
  40. package/dist/esm/core/theme-validation.js +5 -3
  41. package/dist/esm/decorator/app.decorator.js +32 -29
  42. package/dist/esm/decorator/controller.decorator.js +21 -12
  43. package/dist/esm/decorator/cooldown.decorator.js +20 -13
  44. package/dist/esm/decorator/defer.decorator.js +3 -2
  45. package/dist/esm/decorator/event.decorator.js +4 -4
  46. package/dist/esm/decorator/filter.decorator.js +1 -0
  47. package/dist/esm/decorator/guard.decorator.js +1 -0
  48. package/dist/esm/decorator/interceptor.decorator.js +2 -0
  49. package/dist/esm/decorator/observer.decorator.js +4 -2
  50. package/dist/esm/decorator/theme.decorator.js +3 -2
  51. package/dist/esm/decorator/validation.decorator.js +7 -4
  52. package/dist/esm/package.json.js +1 -1
  53. package/dist/esm/testing/discord-defaults.js +145 -0
  54. package/dist/esm/testing/meocord-testing-module.js +9 -4
  55. package/dist/esm/testing/mock-interaction.js +73 -34
  56. package/dist/esm/util/refusal.util.js +78 -0
  57. package/dist/esm/util/shutdown-timeout.util.js +3 -0
  58. package/dist/esm/util/source-path.util.js +21 -0
  59. package/dist/types/common/index.d.cts +14 -11
  60. package/dist/types/common/index.d.ts +14 -11
  61. package/dist/types/core/index.d.cts +4 -2
  62. package/dist/types/core/index.d.ts +4 -2
  63. package/dist/types/decorator/index.d.cts +12 -6
  64. package/dist/types/decorator/index.d.ts +12 -6
  65. package/dist/types/{index-BmSDfetA.d.ts → index-CI7kQPXk.d.ts} +34 -4
  66. package/dist/types/{index-CrcfxIXr.d.cts → index-darLQYaE.d.cts} +34 -4
  67. package/dist/types/interface/index.d.cts +1 -1
  68. package/dist/types/interface/index.d.ts +1 -1
  69. package/dist/types/testing/index.d.cts +26 -14
  70. package/dist/types/testing/index.d.ts +26 -14
  71. package/dist/types/{theme-resolvers-CJq6aGG8.d.cts → theme-resolvers-kvBzQHrv.d.cts} +1 -1
  72. package/dist/types/{theme-resolvers-CWU3_PhU.d.ts → theme-resolvers-zrU0w_Bc.d.ts} +1 -1
  73. package/dist/types/{translator-B2F8HOge.d.cts → translator-CmWsC1-t.d.cts} +1 -1
  74. package/dist/types/{translator-CAOse4oQ.d.ts → translator-SbY8yk6y.d.ts} +1 -1
  75. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # meocord
2
2
 
3
+ ## 4.1.0-beta.8
4
+
5
+ ### Minor Changes
6
+
7
+ - [#273](https://github.com/meocord/meocord/pull/273) [`ebd546e`](https://github.com/meocord/meocord/commit/ebd546efe1327d1a4353ea014875ba87aad956a7) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A message command can tell its author, in a direct message, what the channel doesn't show. Without a filter, a message command's unexpected error is only logged and a cooldown refusal is skipped silently, since a reply in the channel can't be private. Two options turn on a direct message instead, both off by default:
8
+
9
+ - `@MeoCord({ messages: { dmOnError: true } })` tells the author the command failed, naming the command, the channel and the server. The error is still logged.
10
+ - `@MeoCord({ messages: { dmOnCooldown: true } })` tells the author how long to wait, once per wait: retries before it ends send nothing more. The notice is counted in the app's cooldown store, so it holds across shards with a shared store.
11
+
12
+ Only patterned message handlers are answered, and only when no filter handled the error. A command sent in a direct message is answered there. A member whose direct messages are closed is not told, at debug level. Guard, validation, usage and `UserError` replies are unchanged. The texts are `meocord.dm.error` and `meocord.dm.cooldown`, translatable like MeoCord's other texts, in the server's language.
13
+
14
+ - [#269](https://github.com/meocord/meocord/pull/269) [`a18e948`](https://github.com/meocord/meocord/commit/a18e9482ca6423bb588d967010bb4125126b351b) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/testing`: `createMockUser(props?)` and `createMockChannel(Class, props?)` take values for the mock's properties, as `createMockInteraction` does, so `createMockUser({ bot: true })` makes a bot and `createMockChannel(TextChannel, { name: 'general' })` a named channel. Neither took any before, so a bot user took `Object.assign`. The managers of a mock channel, `messages`, `threads` and a thread's `members`, and a mock guild's `bans`, have a real, empty `cache`, as a guild's `members`, `roles` and `channels` do, where reading `cache.size` gave a stub object.
15
+
16
+ ### Patch Changes
17
+
18
+ - [#274](https://github.com/meocord/meocord/pull/274) [`4827e0f`](https://github.com/meocord/meocord/commit/4827e0f81536816df741af2ef25e430fc19c7292) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord start --dev` runs one bot at a time. A build that finished while the previous bot was still shutting down, which happens when the watcher reports one change twice or a file changes again during a slow `onShutdown`, started a second bot at once, alongside the one stopping. Once that one exited, a third started. The second was left running and connected to Discord, and a restart or Ctrl+C no longer reached it. Now builds that finish during a restart start a single bot from the latest build, once the previous one has exited, and a Ctrl+C during a restart waits for the stopping bot and starts nothing.
19
+
20
+ - [#266](https://github.com/meocord/meocord/pull/266) [`6be7120`](https://github.com/meocord/meocord/commit/6be71205ec145a730137135dc92b76bd70a30cf6) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord start --dev` restarts the bot once per change, and the new bot always starts. The watcher took the tsconfig copy MeoCord writes for each build for a change of its own and rebuilt right after the first build, restarting the bot while it was still logging in. A stop that lands during login also left the old process running: discord.js's `destroy()` does not settle while the gateway waits for READY, so the bot came online after "Shutting down bot..." and the replacement never started.
21
+
22
+ - A stop during login now ends the start at once, and ends with "Bot has shut down" as any other stop does: no `onReady` hook runs, `start()` rejects with an error `isExplainedError()` recognises, and the process exits 0. The client is closed once its login completes. This also fixes a Ctrl+C during login in production, which did nothing until a second one forced exit 1.
23
+ - Edits to `tsconfig.json` now rebuild and restart the bot under `start --dev`, as edits to `meocord.config.ts` do. A changed `meocord.config.ts` is compiled again before the rebuild, and one that does not compile, such as a file saved mid-edit, is reported and leaves the running bot in place instead of ending the session.
24
+ - If an application still has not exited after its `shutdownTimeout` and a short grace period, `start --dev` kills it with a warning and starts the new build.
25
+
26
+ - [#265](https://github.com/meocord/meocord/pull/265) [`2d1ca03`](https://github.com/meocord/meocord/commit/2d1ca03384306a45ae34912652738a3eae80d5a2) Thanks [@l7aromeo](https://github.com/l7aromeo)! - An answer MeoCord fails to build is reported as a fault, not passed off as Discord refusing it. When the text of a usage reply, a `UserError` reply or the answer to an interaction's error cannot be written, or a presenter throws while building an error view, the bot logs an error naming the call, and observers are told the call ended in `'error'` with that fault. Before, it was logged only at debug level as "Could not reply", and the message went unanswered with no trace at the default log level. A send that fails is logged at debug level only when Discord refused it, a `DiscordAPIError` such as a missing permission; any other failure is logged as an error. In a testing module, such a fault rejects `dispatch()`. `respond(interaction).error()` still never throws, and logs a presenter that fails as an error.
27
+
28
+ - [#275](https://github.com/meocord/meocord/pull/275) [`7d70abf`](https://github.com/meocord/meocord/commit/7d70abfbc1b1687ef6ea7d97167be26d7ee08fa4) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Two JSDoc corrections. `@On` and `@Once` said an event handler runs with the app's global guards, interceptors and filters; its class's and its own run too, as they always have. `MeoCordTestingModule.fromApp`'s remarks now say plainly that `compile()` runs no factory and `init()` runs each one the module provides, so a factory the test replaces never runs, and one it keeps runs at `init()`.
29
+
30
+ - [#270](https://github.com/meocord/meocord/pull/270) [`9cb86a4`](https://github.com/meocord/meocord/commit/9cb86a4db294fe31cb0cfbbc47daa8a627e5ef25) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A mistake MeoCord refuses as the bot loads is reported as one line, and the bot exits 1. This covers an invalid customId pattern, a builder that fails, a `@Cooldown` it cannot count, and what `MeoCordFactory.create()` refuses, such as a message pattern or two handlers for one command. Before, each came out as Node's uncaught-exception report: MeoCord's own source line, a stack through its bundle, and often none of it pointing at your file. Now the line names the handler, and the source file where the stack shows it. Under `start --dev` the watch session keeps running, so the next edit rebuilds. Any other error keeps the runtime's own report.
31
+
32
+ - Every refusal begins with what it is on, `Class.method:`, `Class:` or `App:` for `@MeoCord` options, followed by the decorator and the problem, such as `SampleButtonController.handleButtonWithId: Invalid pattern …` or `App: @MeoCord({ guards }): null is not a class.` A test that matches a refusal's whole text may need its expected message updated.
33
+ - `@Cooldown`, `@Validate` given something that isn't a Standard Schema, and `SetMetadata` with a key MeoCord reserves now throw where they are applied, rather than where they are called, so the message can name `Class.method`. Written as decorators, both happen in the same statement. A composite made with `applyDecorators` that includes one of them throws where it is applied; one that is defined but never applied no longer throws.
34
+ - New projects' `src/main.ts` calls `MeoCordFactory.create()` inside `bootstrap()`, and sets exit code 1 for any startup error. An existing `main.ts` needs no change: `create()` reports what it refuses itself, and marks it so `isExplainedError()` returns `true`.
35
+
36
+ - [#268](https://github.com/meocord/meocord/pull/268) [`5a69c71`](https://github.com/meocord/meocord/commit/5a69c710522fed8dfdb46f8e1f436fc68ee8632e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - `meocord/testing`: mocks read the data Discord always sends as Discord sends it. A mock guild's `preferredLocale` was an empty stub object, so `t.forGuild(createMockGuild())`, and a message command's usage reply on a mock message, came out in the translator's default language whatever the test meant. It is now `'en-US'`, and a guild's `name`, a user's `username`, a message's `pinned`, a channel's `type` and the rest have Discord's values: `false` for a flag, `null` for what may be absent, and snowflake ids. `tag`, `displayName`, `createdAt` and `url` are computed from them as discord.js does. `createMockGuild` takes `name` and `preferredLocale`, so `createMockGuild({ preferredLocale: Locale.Indonesian })` gives a server that speaks Indonesian.
37
+
38
+ An interaction with a `guildId` has its user as its `member`, and its `guildLocale` is the `preferredLocale` of the `guild` it is given, as Discord sends it, where it was always `'en-US'`. A mock message has its author as its `member` in a server, and `null` in a direct message, and its `inGuild()` answers as discord.js's does, where it returned `undefined`. A test that reads a property a mock has no value for now gets that value instead of a stub object; one that relied on the stub there sets the property itself. `commandName`, `customId` and a message's `content` stay the test's to give.
39
+
40
+ - [#272](https://github.com/meocord/meocord/pull/272) [`a8f454d`](https://github.com/meocord/meocord/commit/a8f454d20a3f8dc141e90f9aafae5a39caa8761f) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The JSDoc says where every pipeline stage runs, so the API reference can show it on each entry. `@Guard`, `@Interceptor`, `@Catch` and `@Pipe` gain the `@pipeline` tag their `@UseGuard`, `@UseInterceptor`, `@UseFilter` and `@UsePipe` counterparts already had, and so do `GuardInterface`, `InterceptorInterface`, `ExceptionFilter`, `PipeInterface` and `DispatchObserver`. The stage names are those of the pipeline figure on meocord.dev: `@Observer` runs at `observers-start` and `observers-settled`, and `@Defer`'s second step at `defer-lock`.
41
+
42
+ - [#263](https://github.com/meocord/meocord/pull/263) [`925225d`](https://github.com/meocord/meocord/commit/925225de08e6a25d55760af6cb9501dc27854b0e) Thanks [@l7aromeo](https://github.com/l7aromeo)! - The JSDoc of `ContextMenuCommandBuilder.setType`, as MeoCord types it, says to leave a context menu builder's `build()` return type inferred. Written out as `ContextMenuCommandBuilder`, it drops the kind `setType()` gave, so a handler of the other kind compiles and is caught only as the bot starts.
43
+
44
+ - [#271](https://github.com/meocord/meocord/pull/271) [`8b1d9db`](https://github.com/meocord/meocord/commit/8b1d9db951b6c89225734c82336a3dba3b672c9c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - A new app's `package.json` has a `start` script, `meocord start --prod`, so `npm start`, `bun run start` and a host that runs `npm start` for you, as many Node hosts do by default, start the production build. `start:prod` is unchanged, and building stays its own step: run `build:prod` before `start`. An existing app can add the line to its `scripts` to get the same.
45
+
3
46
  ## 4.1.0-beta.7
4
47
 
5
48
  ### Minor Changes
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  require('reflect-metadata');
4
- var themeResolvers = require('./theme-resolvers-B4BV5Ikl.cjs');
4
+ var themeResolvers = require('./theme-resolvers-CVm6n5-Q.cjs');
5
5
  var discord_js = require('discord.js');
6
6
  var enum_index = require('../enum/index.cjs');
7
7
  var node_async_hooks = require('node:async_hooks');
@@ -14,11 +14,11 @@ var inversify = require('inversify');
14
14
  */ function defineStageTypes(cls, types, decorator) {
15
15
  if (!types) return;
16
16
  if (types.length === 0) {
17
- throw new Error(`@${decorator}({ types: [] }) on ${cls.name} lists no types, so it would never run. List the types it ` + `runs for, or leave types out to run for every type.`);
17
+ throw themeResolvers.refuse(new Error(`${cls.name}: @${decorator}({ types: [] }) lists no types, so it would never run. List the types it ` + `runs for, or leave types out to run for every type.`));
18
18
  }
19
19
  // Interceptors never run for autocomplete, which must answer within three seconds
20
20
  if (decorator === 'Interceptor' && types.every((type)=>type === 'autocomplete')) {
21
- throw new Error(`@Interceptor({ types: ['autocomplete'] }) on ${cls.name} can never run: interceptors skip autocomplete ` + `handlers. List the types it should run for instead.`);
21
+ throw themeResolvers.refuse(new Error(`${cls.name}: @Interceptor({ types: ['autocomplete'] }) can never run: interceptors skip autocomplete ` + `handlers. List the types it should run for instead.`));
22
22
  }
23
23
  Reflect.defineMetadata(STAGE_TYPES, [
24
24
  ...types
@@ -46,7 +46,7 @@ var inversify = require('inversify');
46
46
  const reason = malformation(entry);
47
47
  if (!reason) continue;
48
48
  const Kind = `${kind[0].toUpperCase()}${kind.slice(1)}`;
49
- throw new Error(`${decorator} on ${where}: ${reason}. Give ${kind === 'interceptor' ? 'an' : 'a'} ${kind} class, or ` + `{ provide: ${Kind}Class, params? } with params an object.`);
49
+ throw themeResolvers.refuse(new Error(`${where}: ${decorator}: ${reason}. Give ${kind === 'interceptor' ? 'an' : 'a'} ${kind} class, or ` + `{ provide: ${Kind}Class, params? } with params an object.`));
50
50
  }
51
51
  }
52
52
  /** The class of a stage entry. */ function stageClass(entry) {
@@ -80,8 +80,8 @@ function eventDecorator(event, once) {
80
80
  * `@ReactionHandler`, which route them.
81
81
  *
82
82
  * @remarks
83
- * The handler's parameters are typed from discord.js's `ClientEvents`. It runs through the pipeline, so global
84
- * guards apply to it, and an error it throws is logged without stopping the bot. Handling `interactionCreate`
83
+ * The handler's parameters are typed from discord.js's `ClientEvents`. It runs through the pipeline, so the app's
84
+ * global guards apply to it, and its class's and its own, and an error it throws is logged without stopping the bot. Handling `interactionCreate`
85
85
  * or `messageCreate` here runs alongside MeoCord's own dispatch of them.
86
86
  *
87
87
  * @param event - The client event to handle, such as `'guildMemberAdd'`.
@@ -94,7 +94,7 @@ function eventDecorator(event, once) {
94
94
  * }
95
95
  * ```
96
96
  *
97
- * @pipeline handler with the global guards, interceptors and filters that apply to events
97
+ * @pipeline handler with the guards, interceptors and filters that apply to events: the app's global ones, its class's and its own
98
98
  * @group Decorators
99
99
  * @category Handlers
100
100
  * @see {@link Once}
@@ -121,7 +121,7 @@ function eventDecorator(event, once) {
121
121
  * }
122
122
  * ```
123
123
  *
124
- * @pipeline handler with the global guards, interceptors and filters that apply to events
124
+ * @pipeline handler with the guards, interceptors and filters that apply to events: the app's global ones, its class's and its own
125
125
  * @group Decorators
126
126
  * @category Handlers
127
127
  * @see {@link On}
@@ -174,7 +174,7 @@ function eventDecorator(event, once) {
174
174
  * The error for a class resolved once and shared that asks for the per-call `ExecutionContext`,
175
175
  * which would keep the first call's context for every later one.
176
176
  */ function singletonContextError(cls) {
177
- return new Error(`${cls.name || 'A class'} is resolved once and shared, so it cannot inject ExecutionContext: it would keep ` + `the first call's context for every later call. Inject ExecutionContext only into guards.`);
177
+ return new Error(`${cls.name || 'A class'}: resolved once and shared, so it cannot inject ExecutionContext: it would keep ` + `the first call's context for every later call. Inject ExecutionContext only into guards.`);
178
178
  }
179
179
  const needsContextCache = new WeakMap();
180
180
  /**
@@ -632,6 +632,7 @@ const logger$1 = new themeResolvers.Logger('Cooldown');
632
632
  }
633
633
  return counted;
634
634
  }
635
+ /** The store key of the cooldown each refusal came from, for {@link claimCooldownNotice}. */ const refusalKeys = new WeakMap();
635
636
  /** Asks the store about the call under the app's policy, and throws what the answer means for it. */ async function ask(container, counted, peek, call) {
636
637
  const store = cooldownStoreOf(container);
637
638
  const policy = cooldownPolicyOf(container);
@@ -652,7 +653,35 @@ const logger$1 = new themeResolvers.Logger('Cooldown');
652
653
  throw failure;
653
654
  }
654
655
  reportRecovery(store);
655
- if (!verdict.allowed) throw new themeResolvers.CooldownError(verdict.retryAfterMs, counted[verdict.blocked ?? 0]?.per ?? counted[0].per);
656
+ if (verdict.allowed) return;
657
+ const blocking = counted[verdict.blocked ?? 0] ?? counted[0];
658
+ const refusal = new themeResolvers.CooldownError(verdict.retryAfterMs, blocking.per);
659
+ refusalKeys.set(refusal, blocking.key);
660
+ throw refusal;
661
+ }
662
+ /**
663
+ * Whether the caller a cooldown refused is yet to be told so during this wait: a one-use cooldown in the same
664
+ * store, keyed on the refusing cooldown's own key, over the wait left. The first refusal of a wait claims it;
665
+ * every retry in that wait is refused it, and the next wait claims its own. A refusal `@Cooldown` did not
666
+ * make, or a store that fails to answer, claims nothing.
667
+ */ async function claimCooldownNotice(container, refusal) {
668
+ const key = refusalKeys.get(refusal);
669
+ if (key === undefined) return false;
670
+ const notice = {
671
+ key: `${key}:notice`,
672
+ limit: {
673
+ uses: 1,
674
+ windowMs: Math.max(1, refusal.retryAfterMs)
675
+ }
676
+ };
677
+ try {
678
+ return (await askWithin(cooldownStoreOf(container), [
679
+ notice
680
+ ], cooldownPolicyOf(container).timeoutMs, false)).allowed;
681
+ } catch (error) {
682
+ logger$1.debug(`Could not ask the cooldown store whether to tell a refused caller: ${String(error.cause ?? error)}`);
683
+ return false;
684
+ }
656
685
  }
657
686
  /** Whether the call is one cooldowns count: an interaction's or a message's. */ function isCounted(contextOf) {
658
687
  // Only a controller's own cooldowns reach other handlers, and those are not counted there.
@@ -707,7 +736,7 @@ const logger$1 = new themeResolvers.Logger('Cooldown');
707
736
  return undefined;
708
737
  }
709
738
  /** The error for `@Defer` on a handler that cannot be deferred. */ function deferMisuseError(className, methodName, kind) {
710
- return new Error(`@Defer is for interaction handlers, but ${className}.${methodName} is ${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} ` + `handler. Remove @Defer from it.`);
739
+ return new Error(`${className}.${methodName}: @Defer is for interaction handlers, and this is ${kind === 'autocomplete' || kind === 'event' ? 'an' : 'a'} ${kind} ` + `handler. Remove @Defer from it.`);
711
740
  }
712
741
  /**
713
742
  * `@Defer`'s first step: acknowledge now, or for `'auto'`, at the earlier of `receivedAt + after` and
@@ -769,7 +798,7 @@ function isInterceptorWithParams(entry) {
769
798
  });
770
799
  /** Binds `cls` and its unbound dependencies as singletons, refusing any that injects `ExecutionContext`. */ function bindShared(container, cls) {
771
800
  if (container.isBound(cls)) return;
772
- if (injectedTokens(cls).includes(themeResolvers.ExecutionContext)) throw singletonContextError(cls);
801
+ if (injectedTokens(cls).includes(themeResolvers.ExecutionContext)) throw themeResolvers.refuse(singletonContextError(cls));
773
802
  themeResolvers.makeInjectable(cls);
774
803
  container.bind(cls).toSelf().inSingletonScope();
775
804
  for (const dep of injectedTokens(cls)){
@@ -847,7 +876,7 @@ function filterClass(entry) {
847
876
  /** Binds a filter as a singleton, after checking it is one. */ function prepareFilter(container, entry) {
848
877
  const cls = filterClass(entry);
849
878
  if (!Reflect.hasOwnMetadata(CATCH_TYPES, cls)) {
850
- throw new Error(`${cls.name || 'A filter'} is used as an exception filter but is not decorated with @Catch().`);
879
+ throw themeResolvers.refuse(new Error(`${cls.name || 'A filter'}: used as an exception filter, but not decorated with @Catch().`));
851
880
  }
852
881
  bindShared(container, cls);
853
882
  }
@@ -961,7 +990,7 @@ const logger = new themeResolvers.Logger('Observer');
961
990
  for (const observer of observers){
962
991
  if (typeof observer !== 'function' || !Reflect.getMetadata(OBSERVER_CLASS, observer)) {
963
992
  const name = typeof observer === 'function' ? observer.name : String(observer);
964
- throw new Error(`${where} takes classes decorated with @Observer(), not ${name}.`);
993
+ throw themeResolvers.refuse(new Error(`${where} takes classes decorated with @Observer(), not ${name}.`));
965
994
  }
966
995
  }
967
996
  }
@@ -1915,11 +1944,11 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
1915
1944
  // A class listed alone, as Nest takes one: say where a class goes instead
1916
1945
  if (typeof entry === 'function') {
1917
1946
  const name = tokenName(entry);
1918
- const listing = where.startsWith('@MeoCord') ? 'list a service in @MeoCord({ services })' : 'a class a controller or service injects is bound for you, so it needs no listing';
1919
- throw new Error(`${name} in ${where} is a class, not a provider: ${listing}. To put something in its place, write ` + `{ provide: ${name}, useValue } or { provide: ${name}, useClass }.`);
1947
+ const listing = where.includes('@MeoCord') ? 'list a service in @MeoCord({ services })' : 'a class a controller or service injects is bound for you, so it needs no listing';
1948
+ throw themeResolvers.refuse(new Error(`${where}: ${name} is a class, not a provider: ${listing}. To put something in its place, write ` + `{ provide: ${name}, useValue } or { provide: ${name}, useClass }.`));
1920
1949
  }
1921
1950
  if (!entry || typeof entry !== 'object' || !isToken(entry.provide)) {
1922
- throw new Error(`${where} has a provider without a token: set provide to a class, a string or a symbol.`);
1951
+ throw themeResolvers.refuse(new Error(`${where}: a provider has no token: set provide to a class, a string or a symbol.`));
1923
1952
  }
1924
1953
  const name = tokenName(entry.provide);
1925
1954
  const kinds = [
@@ -1928,20 +1957,20 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
1928
1957
  'useFactory'
1929
1958
  ].filter((kind)=>kind in entry);
1930
1959
  if (kinds.length !== 1) {
1931
- throw new Error(`The provider for ${name} in ${where} needs exactly one of useValue, useClass and useFactory.`);
1960
+ throw themeResolvers.refuse(new Error(`${where}: the provider for ${name} needs exactly one of useValue, useClass and useFactory.`));
1932
1961
  }
1933
1962
  if ('useClass' in entry && typeof entry.useClass !== 'function') {
1934
- throw new Error(`The provider for ${name} in ${where} has a useClass that is not a class.`);
1963
+ throw themeResolvers.refuse(new Error(`${where}: the provider for ${name} has a useClass that is not a class.`));
1935
1964
  }
1936
1965
  if ('useFactory' in entry) {
1937
1966
  if (typeof entry.useFactory !== 'function') {
1938
- throw new Error(`The provider for ${name} in ${where} has a useFactory that is not a function.`);
1967
+ throw themeResolvers.refuse(new Error(`${where}: the provider for ${name} has a useFactory that is not a function.`));
1939
1968
  }
1940
1969
  if (entry.inject !== undefined && !(Array.isArray(entry.inject) && entry.inject.every(isToken))) {
1941
- throw new Error(`The provider for ${name} in ${where} has an inject that is not a list of tokens.`);
1970
+ throw themeResolvers.refuse(new Error(`${where}: the provider for ${name} has an inject that is not a list of tokens.`));
1942
1971
  }
1943
1972
  }
1944
- if (map.has(entry.provide)) throw new Error(`${name} is provided twice in ${where}.`);
1973
+ if (map.has(entry.provide)) throw themeResolvers.refuse(new Error(`${where}: ${name} is provided twice.`));
1945
1974
  map.set(entry.provide, provider);
1946
1975
  }
1947
1976
  return map;
@@ -1957,7 +1986,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
1957
1986
  }
1958
1987
  if (isClassProvider(provider)) {
1959
1988
  const cls = provider.useClass;
1960
- if (injectedTokens(cls).includes(themeResolvers.ExecutionContext)) throw singletonContextError(cls);
1989
+ if (injectedTokens(cls).includes(themeResolvers.ExecutionContext)) throw themeResolvers.refuse(singletonContextError(cls));
1961
1990
  themeResolvers.makeInjectable(cls);
1962
1991
  container.bind(token).to(cls).inSingletonScope();
1963
1992
  for (const dependency of injectedTokens(cls))if (isAppClassToken(dependency)) bindClass(dependency);
@@ -1977,14 +2006,14 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
1977
2006
  for (const cls of classes){
1978
2007
  const token = injectedTokens(cls).find(missing);
1979
2008
  if (token !== undefined) {
1980
- throw new Error(`${cls.name} injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`);
2009
+ throw themeResolvers.refuse(new Error(`${cls.name}: it injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
1981
2010
  }
1982
2011
  }
1983
2012
  for (const [provided, provider] of providers){
1984
2013
  const dependencies = isFactoryProvider(provider) ? provider.inject ?? [] : isClassProvider(provider) ? injectedTokens(provider.useClass) : [];
1985
2014
  const token = dependencies.find(missing);
1986
2015
  if (token !== undefined) {
1987
- throw new Error(`The provider for ${tokenName(provided)} injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`);
2016
+ throw themeResolvers.refuse(new Error(`${tokenName(provided)}: its provider injects ${tokenName(token)}, which nothing provides: add a provider for it to ${where}.`));
1988
2017
  }
1989
2018
  }
1990
2019
  }
@@ -2019,7 +2048,7 @@ const isToken = (value)=>typeof value === 'function' || typeof value === 'string
2019
2048
  if (index === -1) continue;
2020
2049
  const injectors = classes.filter((other)=>other !== cls && injectedTokens(other).includes(cls)).map((other)=>other.name);
2021
2050
  const injectedBy = injectors.length === 0 ? '' : ` (${injectors.length === 1 ? injectors[0] : `${injectors.slice(0, -1).join(', ')} and ${injectors.at(-1)}`} ` + `inject${injectors.length === 1 ? 's' : ''} ${cls.name})`;
2022
- throw new Error(`${cls.name} cannot be created: parameter ${index + 1} of its constructor has no runtime type. Usually ` + `${cls.name} and a class it injects import each other${injectedBy}, or the parameter is typed with an ` + 'interface or an `import type`. Move what they both need into a third service, or inject the parameter ' + 'with @Inject(token).');
2051
+ throw themeResolvers.refuse(new Error(`${cls.name}: parameter ${index + 1} of its constructor has no runtime type, so it cannot be created. Usually ` + `${cls.name} and a class it injects import each other${injectedBy}, or the parameter is typed with an ` + 'interface or an `import type`. Move what they both need into a third service, or inject the parameter ' + 'with @Inject(token).'));
2023
2052
  }
2024
2053
  }
2025
2054
  /**
@@ -2104,6 +2133,7 @@ exports.bindShared = bindShared;
2104
2133
  exports.callFilter = callFilter;
2105
2134
  exports.callGuardedHandler = callGuardedHandler;
2106
2135
  exports.claimAmbientAppTheme = claimAmbientAppTheme;
2136
+ exports.claimCooldownNotice = claimCooldownNotice;
2107
2137
  exports.classChain = classChain;
2108
2138
  exports.classLevelGuards = classLevelGuards;
2109
2139
  exports.classUnits = classUnits;